419 lines
12 KiB
Vue
419 lines
12 KiB
Vue
<template>
|
||
<div class="upload-file">
|
||
<el-upload
|
||
ref="uploadRef"
|
||
v-loading="loading"
|
||
v-bind="$attrs"
|
||
:file-list="fileList"
|
||
drag
|
||
:disabled="disabled"
|
||
:multiple="limit > 1"
|
||
:on-error="handleUploadError"
|
||
:on-change="handleChange"
|
||
:auto-upload="false"
|
||
:show-file-list="listType === 'picture-card' ? true : false"
|
||
:list-type="listType"
|
||
:on-preview="handlePictureCardPreview"
|
||
:on-remove="handleRemove"
|
||
>
|
||
<slot>
|
||
<el-button type="primary" :disabled="disabled">选择文件</el-button>
|
||
</slot>
|
||
<template v-if="showTip" #tip>
|
||
<div class="m-t-10px leading-24px">
|
||
<slot name="des">
|
||
<div>
|
||
<!-- <span>附件内容说明:</span> -->
|
||
<span class="text-[#999999]">{{ tips }}</span>
|
||
</div>
|
||
</slot>
|
||
<!-- <slot name="tips">
|
||
<div>
|
||
<span>请上传 大小不超过</span>
|
||
<span class="color-#FA5940 m-l-2px m-r-2px">{{ size }}MB</span>
|
||
<span>格式为</span>
|
||
<span class="color-#FA5940 m-l-2px m-r-2px">{{ fileType.join('/') }}</span>
|
||
<span>的文件</span>
|
||
</div>
|
||
</slot> -->
|
||
</div>
|
||
</template>
|
||
<template v-if="listType === 'picture-card'" #file="{ file }">
|
||
<div v-if="!handelFileType(file.name)" class="custom-preview-card">
|
||
<div class="file-type-icon cursor-pointer" @click="handlePictureCardDown(file)">
|
||
<el-icon :size="32"><Document /></el-icon>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-upload>
|
||
<div v-if="loading" class="text-12px color-#999">{{ loadingtext }}</div>
|
||
|
||
<transition-group v-if="listType !== 'picture-card'" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
|
||
<li v-for="(file, index) in fileList" :key="file.uid" class="el-upload-list__item ele-upload-list__item-content">
|
||
<div class="ele-upload-list__item-content upload-item">
|
||
<div class="curpor-pointer" @click="handlePictureCardDown(file)">
|
||
<span class="el-icon-document cursor-pointer pl-9px">{{ file.name }} </span>
|
||
</div>
|
||
<div class="ele-upload-list__item-content-action">
|
||
<el-link :underline="false" type="danger" :disabled="disabled" @click="handleDelete(index)">删除 </el-link>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
</transition-group>
|
||
|
||
<el-image-viewer v-if="dialogVisible" :url-list="[dialogImageUrl]" @close="dialogVisible = false" />
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import type { PropType } from 'vue'
|
||
import { ref } from 'vue'
|
||
import { uploadV2, creatFile } from '~/api/common/index'
|
||
import { Document } from '@element-plus/icons-vue'
|
||
import type { UploadUserFile, UploadInstance } from 'element-plus'
|
||
import { accDiv } from '~/utils/utils'
|
||
// import CryptoJS from 'crypto-js' // 引入 crypto-js
|
||
import dayjs from 'dayjs'
|
||
|
||
const props = defineProps({
|
||
disabled: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
/** 文件个数限制 */
|
||
limit: {
|
||
type: Number,
|
||
default: 1,
|
||
},
|
||
/** 文件大小限制(M) */
|
||
size: {
|
||
type: Number,
|
||
default: 100,
|
||
},
|
||
/** 文件类型 */
|
||
fileType: {
|
||
type: Array as PropType<string[]>,
|
||
// default: () => ['png', 'jpg', 'jpeg', 'doc', 'xls', 'ppt', 'pdf', 'txt'],
|
||
default: () => [],
|
||
},
|
||
/** 上传地址 */
|
||
uploadUrl: {
|
||
type: String,
|
||
default: '/prod-api/app-api/infra/file/presigned-url',
|
||
},
|
||
/** 提示文案 */
|
||
tips: {
|
||
type: String,
|
||
default: '提供佐证变更有效的证明文件(邮件/聊天截图/情况说明)',
|
||
},
|
||
/** 提示文案 */
|
||
showTip: {
|
||
type: Boolean,
|
||
default: true,
|
||
},
|
||
/** 是否预览cad */
|
||
cadShow: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
/** 组件类型 */
|
||
listType: {
|
||
type: String as PropType<'text' | 'picture' | 'picture-card'>,
|
||
default: 'text', // picture" | "text" | "picture-card
|
||
},
|
||
})
|
||
|
||
const emit = defineEmits(['validate', 'preview'])
|
||
const uploadRef = ref<UploadInstance>()
|
||
const fileList = defineModel<any>('fileList', {
|
||
default: [],
|
||
})
|
||
|
||
const fileChange = ref()
|
||
const handleChange = async (uploadFile: UploadUserFile) => {
|
||
if (uploadFile.status !== 'ready') return
|
||
/** 上传前校验 */
|
||
if (handleBeforeUpload(uploadFile)) {
|
||
fileChange.value = uploadFile
|
||
// 新增:清除上传组件内部缓存
|
||
uploadRef.value?.handleRemove(fileChange.value) // 关键:主动触发上传组件的remove方法
|
||
return
|
||
}
|
||
/** 上传 */
|
||
await handleUploadFile(uploadFile)
|
||
}
|
||
/** 上传前校验 */
|
||
const handleBeforeUpload = (rawFile: UploadUserFile) => {
|
||
if (fileList.value.length + 1 > props.limit) {
|
||
handleExceed()
|
||
return true
|
||
}
|
||
if (props.size > 0 && rawFile.size && +accDiv(accDiv(rawFile.size, 1024), 1024) > props.size) {
|
||
ElMessage.error(`文件大小不能超过${props.size}M`)
|
||
return true
|
||
}
|
||
const ext = rawFile.name.split('.').pop()?.toLowerCase()
|
||
if (ext && props.fileType?.length && !props.fileType.includes(ext)) {
|
||
ElMessage.error(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`)
|
||
return true
|
||
}
|
||
}
|
||
const handleExceed = () => {
|
||
ElMessage.error(`最多只能上传 ${props.limit} 个文件`)
|
||
}
|
||
const handleUploadError = (err: any) => {
|
||
ElMessage.error('文件上传失败:' + err.message)
|
||
}
|
||
|
||
/** 上传 */
|
||
const loading = ref(false)
|
||
const loadingtext = ref('上传中...')
|
||
const uploadUrlV2 = ref('')
|
||
const fileUrl = ref('')
|
||
const configId = ref('')
|
||
const path = ref('')
|
||
const getUploadConfig = async (options: UploadUserFile) => {
|
||
path.value = `${options.uid}/${dayjs(new Date()).format('YYYY/MM/DD/HH/mm/ss')}/${options.name}`
|
||
try {
|
||
const response = await uploadV2(props.uploadUrl, { path: path.value })
|
||
if (response.code !== 0) return ElMessage.error(response.message)
|
||
uploadUrlV2.value = response.data.uploadUrl
|
||
fileUrl.value = response.data.url
|
||
configId.value = response.data.configId
|
||
} catch (err) {
|
||
loading.value = false
|
||
console.error('获取上传配置失败:', err)
|
||
alert('获取上传参数失败,请重试')
|
||
}
|
||
}
|
||
|
||
/** 上传到服务器 */
|
||
const UploadFilled = async (options: UploadUserFile) => {
|
||
try {
|
||
const res = await creatFile({
|
||
configId: Number(configId.value),
|
||
path: path.value,
|
||
name: options.name,
|
||
url: fileUrl.value,
|
||
size: options.size as number,
|
||
})
|
||
if (res.code !== 0) return ElMessage.error(res.message)
|
||
ElMessage.success('上传成功')
|
||
const newFile = {
|
||
name: options?.name,
|
||
url: fileUrl.value,
|
||
size: options?.size,
|
||
uid: options.uid,
|
||
title: options?.name,
|
||
fileId: res.data,
|
||
}
|
||
// 更新文件列表
|
||
fileList.value = [...fileList.value, newFile]
|
||
emit('validate')
|
||
} catch (error) {
|
||
console.error('获取上传配置失败:', error)
|
||
alert('creatFile上传失败,请重试')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** fetch 监听不了进度 需要改成xhr */
|
||
const uploadWithProgress = (url: string, file: any, onProgress: (percent: number) => void) => {
|
||
return new Promise((resolve, reject) => {
|
||
const xhr = new XMLHttpRequest()
|
||
xhr.open('PUT', url, true)
|
||
xhr.setRequestHeader('Content-Type', file.raw.type)
|
||
|
||
xhr.upload.onprogress = function (event) {
|
||
if (event.lengthComputable) {
|
||
const percent = Math.round((event.loaded / event.total) * 100)
|
||
onProgress(percent)
|
||
}
|
||
}
|
||
|
||
xhr.onload = function () {
|
||
if (xhr.status >= 200 && xhr.status < 300) {
|
||
resolve(xhr.response)
|
||
} else {
|
||
reject(new Error('上传失败'))
|
||
}
|
||
}
|
||
|
||
xhr.onerror = function () {
|
||
reject(new Error('网络错误'))
|
||
}
|
||
|
||
xhr.send(file.raw)
|
||
})
|
||
}
|
||
const handleUploadFile = async (options: UploadUserFile) => {
|
||
try {
|
||
loading.value = true
|
||
await getUploadConfig(options)
|
||
// 使用PUT或POST方法上传(根据后端要求)
|
||
// const response = await fetch(uploadUrlV2.value, {
|
||
// method: 'PUT', // 或 POST,根据后端预签名URL的规则
|
||
// body: options.raw,
|
||
// headers: {
|
||
// 'Content-Type': (options.raw as Blob).type, // 设置文件类型
|
||
// },
|
||
// // onUploadProgress: (progressEvent: ProgressEvent) => {
|
||
// // const percentCompleted = Math.round((progressEvent.loaded / progressEvent.total) * 100)
|
||
// // loadingtext.value = `上传中...${percentCompleted}%`
|
||
// // },
|
||
// })
|
||
|
||
// if (response.ok) {
|
||
// await UploadFilled(options)
|
||
// // 此时文件已上传至OSS,fileUrl即为访问地址
|
||
// console.log('文件访问URL:', fileUrl.value)
|
||
// } else {
|
||
// loading.value = false
|
||
// throw new Error('上传文件失败')
|
||
// }
|
||
const response = await uploadWithProgress(uploadUrlV2.value, options, (percent) => {
|
||
loadingtext.value = `上传中...${percent}%`
|
||
})
|
||
if (response) {
|
||
// 错误处理
|
||
loading.value = false
|
||
return ElMessage.error(options.name + '上传错误')
|
||
}
|
||
await UploadFilled(options)
|
||
} catch (error) {
|
||
loading.value = false
|
||
ElMessage.error(options.name + '上传错误')
|
||
}
|
||
}
|
||
|
||
/*** 删除 */
|
||
const handleDelete = (index: number) => {
|
||
fileList.value.splice(index, 1)
|
||
emit('validate')
|
||
}
|
||
|
||
const dialogImageUrl = ref('')
|
||
const dialogVisible = ref(false)
|
||
const handleRemove = (uploadFile: any, uploadFiles: any) => {
|
||
console.log(uploadFile, uploadFiles)
|
||
fileList.value = fileList.value.filter((item: any) => item.uid !== uploadFile.uid)
|
||
emit('validate')
|
||
}
|
||
|
||
const handlePictureCardPreview = (uploadFile: any) => {
|
||
dialogImageUrl.value = uploadFile.url
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
const handlePictureCardDown = (uploadFile: any) => {
|
||
if (props.cadShow) {
|
||
emit('preview', uploadFile)
|
||
} else {
|
||
window.open(uploadFile.url)
|
||
}
|
||
}
|
||
// 生成 MD5 唯一标识符
|
||
// const generateMd5 = () => {
|
||
// const timestamp = new Date().getTime() // 获取当前时间戳
|
||
// const randomString = Math.random().toString(36).substring(2) // 生成随机字符串
|
||
// const data = `${timestamp}${randomString}` // 组合时间戳和随机字符串
|
||
// return CryptoJS.MD5(data).toString() // 生成 MD5 哈希值
|
||
// }
|
||
|
||
// 判断是否是图片
|
||
const handelFileType = (fileName: string) => {
|
||
const ext = fileName?.split('.').pop()?.toLowerCase() || ''
|
||
return ['png', 'jpg', 'jpeg'].includes(ext)
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.custom-preview-card {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
.file-type-icon {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -40%);
|
||
}
|
||
}
|
||
.upload-file {
|
||
::v-deep(.el-upload-dragger) {
|
||
border: none !important;
|
||
padding: 0;
|
||
text-align: left;
|
||
}
|
||
}
|
||
|
||
.upload-file-list .el-upload-list__item {
|
||
line-height: 2;
|
||
margin-bottom: 10px;
|
||
position: relative;
|
||
min-width: 361px;
|
||
}
|
||
|
||
.upload-file-list .ele-upload-list__item-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
color: inherit;
|
||
}
|
||
|
||
.upload-item {
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 5px;
|
||
flex: 1;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.ele-upload-list__item-content-action .el-link {
|
||
margin-right: 10px;
|
||
width: 30px;
|
||
}
|
||
|
||
.el-upload-list--picture-card .el-upload-list__item {
|
||
height: 81px !important;
|
||
}
|
||
|
||
// ::v-deep(.el-upload--picture-card) {
|
||
// height: 81px !important;
|
||
// }
|
||
|
||
// ::v-deep(.el-upload-list--picture-card) {
|
||
// .el-upload-list__item {
|
||
// height: 82px !important;
|
||
// }
|
||
// }
|
||
|
||
::v-deep(.el-upload-list--picture-card) {
|
||
.el-upload-list__item {
|
||
width: 161px;
|
||
height: 81px;
|
||
// margin: 0 8px 8px 0;
|
||
}
|
||
.el-upload--picture-card {
|
||
width: 161px;
|
||
height: 81px;
|
||
}
|
||
}
|
||
|
||
::v-deep(.el-loading-spinner) {
|
||
.circular {
|
||
width: 32px !important;
|
||
height: 32px !important;
|
||
margin-top: 5px !important;
|
||
|
||
.path {
|
||
stroke-width: 3px;
|
||
}
|
||
}
|
||
}
|
||
|
||
::v-deep(.el-loading-spinner i) {
|
||
font-size: 32px !important;
|
||
}
|
||
</style>
|