UploadFile.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <template>
  2. <div v-if="!disabled" class="upload-file">
  3. <el-upload
  4. ref="uploadRef"
  5. v-model:file-list="fileList"
  6. :action="uploadUrl"
  7. :auto-upload="autoUpload"
  8. :before-upload="beforeUpload"
  9. :disabled="disabled"
  10. :drag="drag"
  11. :http-request="httpRequest"
  12. :limit="props.limit"
  13. :multiple="props.limit > 1"
  14. :on-error="excelUploadError"
  15. :on-exceed="handleExceed"
  16. :on-preview="handlePreview"
  17. :on-remove="handleRemove"
  18. :on-success="handleFileSuccess"
  19. :show-file-list="true"
  20. class="upload-file-uploader"
  21. name="file"
  22. >
  23. <el-button type="primary">
  24. <Icon icon="ep:upload-filled" />
  25. 选取文件
  26. </el-button>
  27. <template v-if="isShowTip" #tip>
  28. <div style="font-size: 8px">
  29. 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
  30. </div>
  31. <div style="font-size: 8px">
  32. 格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件
  33. </div>
  34. </template>
  35. <template #file="row">
  36. <div class="flex items-center">
  37. <span>{{ row.file.name }}</span>
  38. <div class="ml-10px">
  39. <el-link
  40. :href="row.file.url"
  41. :underline="false"
  42. download
  43. target="_blank"
  44. type="primary"
  45. >
  46. 下载
  47. </el-link>
  48. </div>
  49. <div class="ml-10px">
  50. <el-button link type="danger" @click="handleRemove(row.file)"> 删除</el-button>
  51. </div>
  52. </div>
  53. </template>
  54. </el-upload>
  55. </div>
  56. <!-- 上传操作禁用时 -->
  57. <div v-if="disabled" class="upload-file">
  58. <div v-for="(file, index) in fileList" :key="index" class="flex items-center file-list-item">
  59. <span>{{ file.name }}</span>
  60. <div class="ml-10px">
  61. <el-link :href="file.url" :underline="false" download target="_blank" type="primary">
  62. 下载
  63. </el-link>
  64. </div>
  65. </div>
  66. </div>
  67. </template>
  68. <script lang="ts" setup>
  69. import { propTypes } from '@/utils/propTypes'
  70. import type { UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
  71. import { isString } from '@/utils/is'
  72. import { useUpload } from '@/components/UploadFile/src/useUpload'
  73. import { UploadFile } from 'element-plus/es/components/upload/src/upload'
  74. defineOptions({ name: 'UploadFile' })
  75. const message = useMessage() // 消息弹窗
  76. const emit = defineEmits(['update:modelValue'])
  77. const props = defineProps({
  78. modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
  79. fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // 文件类型, 例如['png', 'jpg', 'jpeg']
  80. fileSize: propTypes.number.def(5), // 大小限制(MB)
  81. limit: propTypes.number.def(5), // 数量限制
  82. autoUpload: propTypes.bool.def(true), // 自动上传
  83. drag: propTypes.bool.def(false), // 拖拽上传
  84. isShowTip: propTypes.bool.def(true), // 是否显示提示
  85. disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false)
  86. directory: propTypes.string.def(undefined) // 上传目录 ==> 非必传(默认为 undefined)
  87. })
  88. // ========== 上传相关 ==========
  89. const uploadRef = ref<UploadInstance>()
  90. const uploadList = ref<UploadUserFile[]>([])
  91. const fileList = ref<UploadUserFile[]>([])
  92. const uploadNumber = ref<number>(0)
  93. const { uploadUrl, httpRequest } = useUpload(props.directory)
  94. // 文件上传之前判断
  95. const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
  96. if (fileList.value.length >= props.limit) {
  97. message.error(`上传文件数量不能超过${props.limit}个!`)
  98. return false
  99. }
  100. let fileExtension = ''
  101. if (file.name.lastIndexOf('.') > -1) {
  102. fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)
  103. }
  104. const isImg = props.fileType.some((type: string) => {
  105. if (file.type.indexOf(type) > -1) return true
  106. return !!(fileExtension && fileExtension.indexOf(type) > -1)
  107. })
  108. const isLimit = file.size < props.fileSize * 1024 * 1024
  109. if (!isImg) {
  110. message.error(`文件格式不正确, 请上传${props.fileType.join('/')}格式!`)
  111. return false
  112. }
  113. if (!isLimit) {
  114. message.error(`上传文件大小不能超过${props.fileSize}MB!`)
  115. return false
  116. }
  117. message.success('正在上传文件,请稍候...')
  118. // 只有在验证通过后才增加计数器
  119. uploadNumber.value++
  120. return true
  121. }
  122. // 处理上传的文件发生变化
  123. // const handleFileChange = (uploadFile: UploadFile): void => {
  124. // uploadRef.value.data.path = uploadFile.name
  125. // }
  126. // 文件上传成功
  127. const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
  128. message.success('上传成功')
  129. // 删除自身
  130. const index = fileList.value.findIndex((item) => item.response?.data === res.data)
  131. fileList.value.splice(index, 1)
  132. uploadList.value.push({ name: res.data, url: res.data })
  133. if (uploadList.value.length == uploadNumber.value) {
  134. fileList.value.push(...uploadList.value)
  135. uploadList.value = []
  136. uploadNumber.value = 0
  137. emitUpdateModelValue()
  138. }
  139. }
  140. // 文件数超出提示
  141. const handleExceed: UploadProps['onExceed'] = (): void => {
  142. message.error(`上传文件数量不能超过${props.limit}个!`)
  143. }
  144. // 上传错误提示
  145. const excelUploadError: UploadProps['onError'] = (): void => {
  146. message.error('导入数据失败,请您重新上传!')
  147. // 上传失败时减少计数器,避免后续上传被阻塞
  148. uploadNumber.value = Math.max(0, uploadNumber.value - 1)
  149. }
  150. // 删除上传文件
  151. const handleRemove = (file: UploadFile) => {
  152. const index = fileList.value.map((f) => f.name).indexOf(file.name)
  153. if (index > -1) {
  154. fileList.value.splice(index, 1)
  155. emitUpdateModelValue()
  156. }
  157. }
  158. const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
  159. console.log(uploadFile)
  160. }
  161. // 监听模型绑定值变动
  162. watch(
  163. () => props.modelValue,
  164. (val: string | string[]) => {
  165. if (!val) {
  166. fileList.value = [] // fix:处理掉缓存,表单重置后上传组件的内容并没有重置
  167. return
  168. }
  169. fileList.value = [] // 保障数据为空
  170. // 情况1:字符串
  171. if (isString(val)) {
  172. fileList.value.push(
  173. ...val.split(',').map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
  174. )
  175. return
  176. }
  177. // 情况2:数组
  178. fileList.value.push(
  179. ...(val as string[]).map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
  180. )
  181. },
  182. { immediate: true, deep: true }
  183. )
  184. // 发送文件链接列表更新
  185. const emitUpdateModelValue = () => {
  186. // 情况1:数组结果
  187. let result: string | string[] = fileList.value.map((file) => file.url!)
  188. // 情况2:逗号分隔的字符串
  189. if (props.limit === 1 || isString(props.modelValue)) {
  190. result = result.join(',')
  191. }
  192. emit('update:modelValue', result)
  193. }
  194. </script>
  195. <style lang="scss" scoped>
  196. .upload-file-uploader {
  197. margin-bottom: 5px;
  198. }
  199. :deep(.upload-file-list .el-upload-list__item) {
  200. position: relative;
  201. margin-bottom: 10px;
  202. line-height: 2;
  203. border: 1px solid #e4e7ed;
  204. }
  205. :deep(.el-upload-list__item-file-name) {
  206. max-width: 250px;
  207. }
  208. :deep(.upload-file-list .ele-upload-list__item-content) {
  209. display: flex;
  210. justify-content: space-between;
  211. align-items: center;
  212. color: inherit;
  213. }
  214. :deep(.ele-upload-list__item-content-action .el-link) {
  215. margin-right: 10px;
  216. }
  217. .file-list-item {
  218. border: 1px dashed var(--el-border-color-darker);
  219. border-radius: 8px;
  220. }
  221. </style>