UploadStep.vue 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. <template>
  2. <el-form ref="formRef" :model="modelData" label-width="0" class="mt-20px">
  3. <el-form-item class="mb-20px">
  4. <div class="w-full">
  5. <div
  6. class="w-full border-2 border-dashed border-[#dcdfe6] rounded-md p-20px text-center hover:border-[#409eff]"
  7. >
  8. <el-upload
  9. ref="uploadRef"
  10. class="upload-demo"
  11. drag
  12. :action="uploadUrl"
  13. :auto-upload="true"
  14. :on-success="handleUploadSuccess"
  15. :on-error="handleUploadError"
  16. :on-change="handleFileChange"
  17. :on-remove="handleFileRemove"
  18. :before-upload="beforeUpload"
  19. :http-request="httpRequest"
  20. :file-list="fileList"
  21. :multiple="true"
  22. :show-file-list="false"
  23. :accept="acceptedFileTypes"
  24. >
  25. <div class="flex flex-col items-center justify-center py-20px">
  26. <el-icon class="text-[48px] text-[#c0c4cc] mb-10px"><upload-filled /></el-icon>
  27. <div class="el-upload__text text-[16px] text-[#606266]"
  28. >拖拽文件至此,或者
  29. <em class="text-[#409eff] not-italic cursor-pointer">选择文件</em></div
  30. >
  31. <div class="el-upload__tip mt-10px text-[#909399] text-[12px]">
  32. 已支持 {{ supportedFileTypes.join('、') }},每个文件不超过 {{ maxFileSize }} MB。
  33. </div>
  34. </div>
  35. </el-upload>
  36. </div>
  37. <div
  38. v-if="modelData.list && modelData.list.length > 0"
  39. class="mt-15px grid grid-cols-1 gap-2"
  40. >
  41. <div
  42. v-for="(file, index) in modelData.list"
  43. :key="index"
  44. class="flex justify-between items-center py-4px px-12px border-l-4 border-l-[#409eff] rounded-sm shadow-sm hover:bg-[#ecf5ff] transition-all duration-300"
  45. >
  46. <div class="flex items-center">
  47. <el-icon class="mr-8px text-[#409eff]"><document /></el-icon>
  48. <span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span>
  49. </div>
  50. <el-button type="danger" link @click="removeFile(index)" class="ml-2">
  51. <el-icon><delete /></el-icon>
  52. </el-button>
  53. </div>
  54. </div>
  55. </div>
  56. </el-form-item>
  57. <!-- 添加下一步按钮 -->
  58. <el-form-item>
  59. <div class="flex justify-end w-full">
  60. <el-button type="primary" @click="handleNextStep" :disabled="!isAllUploaded">
  61. 下一步
  62. </el-button>
  63. </div>
  64. </el-form-item>
  65. </el-form>
  66. </template>
  67. <script lang="ts" setup>
  68. import { PropType, ref, computed, inject, getCurrentInstance, onMounted } from 'vue'
  69. import { Document, Delete } from '@element-plus/icons-vue' // TODO @芋艿:晚点改
  70. import { useMessage } from '@/hooks/web/useMessage'
  71. import { useUpload } from '@/components/UploadFile/src/useUpload'
  72. import { generateAcceptedFileTypes } from '@/utils'
  73. const props = defineProps({
  74. modelValue: {
  75. type: Object as PropType<any>,
  76. required: true
  77. }
  78. })
  79. const emit = defineEmits(['update:modelValue'])
  80. const formRef = ref() // 表单引用
  81. const uploadRef = ref() // 上传组件引用
  82. const parent = inject('parent', null) // 获取父组件实例
  83. const { uploadUrl, httpRequest } = useUpload() // 使用上传组件的钩子
  84. const message = useMessage() // 消息弹窗
  85. const fileList = ref([]) // 文件列表
  86. const uploadingCount = ref(0) // 上传中的文件数量
  87. // 支持的文件类型和大小限制
  88. const supportedFileTypes = [
  89. 'TXT',
  90. 'MARKDOWN',
  91. 'MDX',
  92. 'PDF',
  93. 'HTML',
  94. 'XLSX',
  95. 'XLS',
  96. 'DOC',
  97. 'DOCX',
  98. 'CSV',
  99. 'EML',
  100. 'MSG',
  101. 'PPTX',
  102. 'XML',
  103. 'EPUB',
  104. 'PPT',
  105. 'MD',
  106. 'HTM'
  107. ]
  108. const allowedExtensions = supportedFileTypes.map((ext) => ext.toLowerCase()) // 小写的扩展名列表
  109. const maxFileSize = 15 // 最大文件大小(MB)
  110. // 构建 accept 属性值,用于限制文件选择对话框中可见的文件类型
  111. const acceptedFileTypes = computed(() => generateAcceptedFileTypes(supportedFileTypes))
  112. /** 表单数据 */
  113. const modelData = computed({
  114. get: () => {
  115. return props.modelValue
  116. },
  117. set: (val) => emit('update:modelValue', val)
  118. })
  119. /** 确保 list 属性存在 */
  120. const ensureListExists = () => {
  121. if (!props.modelValue.list) {
  122. emit('update:modelValue', {
  123. ...props.modelValue,
  124. list: []
  125. })
  126. }
  127. }
  128. /** 是否所有文件都已上传完成 */
  129. const isAllUploaded = computed(() => {
  130. return modelData.value.list && modelData.value.list.length > 0 && uploadingCount.value === 0
  131. })
  132. /**
  133. * 上传前检查文件类型和大小
  134. *
  135. * @param file 待上传的文件
  136. * @returns 是否允许上传
  137. */
  138. const beforeUpload = (file) => {
  139. // 1.1 检查文件扩展名
  140. const fileName = file.name.toLowerCase()
  141. const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1)
  142. if (!allowedExtensions.includes(fileExtension)) {
  143. message.error('不支持的文件类型!')
  144. return false
  145. }
  146. // 1.2 检查文件大小
  147. if (!(file.size / 1024 / 1024 < maxFileSize)) {
  148. message.error(`文件大小不能超过 ${maxFileSize} MB!`)
  149. return false
  150. }
  151. // 2. 增加上传中的文件计数
  152. uploadingCount.value++
  153. return true
  154. }
  155. /**
  156. * 文件上传成功处理
  157. *
  158. * @param response 上传响应
  159. * @param file 上传的文件
  160. */
  161. const handleUploadSuccess = (response, file) => {
  162. if (response && response.data) {
  163. // 添加到文件列表
  164. ensureListExists()
  165. emit('update:modelValue', {
  166. ...props.modelValue,
  167. list: [
  168. ...props.modelValue.list,
  169. {
  170. name: file.name,
  171. url: response.data
  172. }
  173. ]
  174. })
  175. } else {
  176. message.error(`文件 ${file.name} 上传失败`)
  177. }
  178. // 减少上传中的文件计数
  179. uploadingCount.value = Math.max(0, uploadingCount.value - 1)
  180. }
  181. /**
  182. * 文件上传失败处理
  183. *
  184. * @param error 错误信息
  185. * @param file 上传的文件
  186. */
  187. const handleUploadError = (error, file) => {
  188. message.error(`文件 ${file.name} 上传失败: ${error}`)
  189. // 减少上传中的文件计数
  190. uploadingCount.value = Math.max(0, uploadingCount.value - 1)
  191. }
  192. /**
  193. * 文件变更处理
  194. *
  195. * @param file 变更的文件
  196. */
  197. const handleFileChange = (file) => {
  198. if (file.status === 'success' || file.status === 'fail') {
  199. uploadingCount.value = Math.max(0, uploadingCount.value - 1)
  200. }
  201. }
  202. /**
  203. * 文件移除处理
  204. *
  205. * @param file 被移除的文件
  206. */
  207. const handleFileRemove = (file) => {
  208. if (file.status === 'uploading') {
  209. uploadingCount.value = Math.max(0, uploadingCount.value - 1)
  210. }
  211. }
  212. /**
  213. * 从列表中移除文件
  214. *
  215. * @param index 要移除的文件索引
  216. */
  217. const removeFile = (index: number) => {
  218. // 从列表中移除文件
  219. const newList = [...props.modelValue.list]
  220. newList.splice(index, 1)
  221. // 更新表单数据
  222. emit('update:modelValue', {
  223. ...props.modelValue,
  224. list: newList
  225. })
  226. }
  227. /** 下一步按钮处理 */
  228. const handleNextStep = () => {
  229. // 1.1 检查是否有文件上传
  230. if (!modelData.value.list || modelData.value.list.length === 0) {
  231. message.warning('请上传至少一个文件')
  232. return
  233. }
  234. // 1.2 检查是否有文件正在上传
  235. if (uploadingCount.value > 0) {
  236. message.warning('请等待所有文件上传完成')
  237. return
  238. }
  239. // 2. 获取父组件的goToNextStep方法
  240. const parentEl = parent || getCurrentInstance()?.parent
  241. if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') {
  242. parentEl.exposed.goToNextStep()
  243. }
  244. }
  245. /** 初始化 */
  246. onMounted(() => {
  247. ensureListExists()
  248. })
  249. </script>
  250. <style lang="scss" scoped></style>