file.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <template>
  2. <div class="input file" :class="{ suffix: $slots.icon, disabled: disabled, valuable }">
  3. <template v-if="valuable">
  4. <slot :key="modelValue" name="valuable" />
  5. </template>
  6. <input v-if="!maxLen || maxLen > modelValue.length" ref="inputRef" title="" class="ui-text" type="file" :accept="accept" :multiple="multiple" @change="selectFileHandler" />
  7. <template v-if="!$slots.replace">
  8. <span class="replace">
  9. <div v-if="!valuable" class="placeholder">
  10. <p><ui-icon type="add" /></p>
  11. <p>{{ placeholder }}</p>
  12. <p class="bottom">
  13. <template v-if="!othPlaceholder">
  14. <template v-if="accept">支持 {{ accept }} 等格式,</template>
  15. <template v-if="normalizeScale">宽*高比例 {{ scale }},</template>
  16. <template v-if="maxSize">大小不超过 {{ sizeStr }}{{ maxLen ? ',' : '' }}</template>
  17. <template v-if="maxLen">个数不超过 {{ maxLen }}个</template>
  18. </template>
  19. <template v-else>
  20. {{ othPlaceholder }}
  21. </template>
  22. </p>
  23. </div>
  24. <span v-else-if="!maxLen || maxLen > modelValue.length">
  25. {{ multiple ? '继续添加' : '替换' }}
  26. </span>
  27. <span v-if="maxLen && modelValue.length" class="tj">
  28. <span>{{ modelValue.length || 0 }}</span> / {{ maxLen }}
  29. </span>
  30. </span>
  31. </template>
  32. <div v-else class="use-replace">
  33. <slot name="replace" />
  34. </div>
  35. </div>
  36. </template>
  37. <script setup lang="ts">
  38. import { computed, defineEmits, defineExpose, defineProps, ref } from 'vue'
  39. import { toRawType } from '@kankan/utils'
  40. import Dialog from '@kankan/components/basic/dialog'
  41. import { fileProps } from './file'
  42. // import Message from '../message';
  43. // import { useI18n } from '@/i18n'
  44. // const { t } = useI18n({ useScope: 'global' })
  45. const props = defineProps(fileProps)
  46. const emit = defineEmits(['update:modelValue'])
  47. const inputRef = ref(null)
  48. const normalizeScale = computed(() => {
  49. if (props.scale) {
  50. const [w, h] = props.scale.split(':')
  51. if (Number(w) && Number(h)) {
  52. return [Number(w), Number(h)]
  53. }
  54. }
  55. return []
  56. })
  57. const valuable = computed(() => (Array.isArray(props.modelValue) ? props.modelValue.length : !!props.modelValue))
  58. const sizeStr = computed(() => props.maxSize && `${props.maxSize / 1024 / 1024}MB`)
  59. const supports = {
  60. image: {
  61. types: ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'],
  62. preview(file, url) {
  63. return new Promise((resolve, reject) => {
  64. const img = new Image()
  65. img.onload = () => resolve([img.width, img.height, file])
  66. img.onerror = reject
  67. img.src = url
  68. })
  69. },
  70. },
  71. video: {
  72. types: ['video/mp4'],
  73. preview(file, url) {
  74. return new Promise((resolve, reject) => {
  75. const video = document.createElement('video')
  76. video.preload = 'metadata'
  77. video.onloadedmetadata = () => resolve([video.videoWidth, video.videoHeight, file])
  78. video.onerror = reject
  79. video.src = url
  80. })
  81. },
  82. },
  83. }
  84. const producePreviews = files =>
  85. Promise.all(
  86. files.map(
  87. file =>
  88. new Promise((resolve, reject) => {
  89. const fr = new FileReader()
  90. fr.onloadend = e => resolve(e.target.result)
  91. fr.onerror = e => loaderror(file, reject(e))
  92. fr.readAsDataURL(file)
  93. })
  94. )
  95. )
  96. const calcScale = (w, h) => Number.parseInt((w / h) * 1000)
  97. const selectFileHandler = async ev => {
  98. const fileEl = ev.target
  99. const files = Array.from(fileEl.files)
  100. const previewError = (e, msg = `预览加载失败!`) => {
  101. console.error(e)
  102. // Message.error(msg)
  103. Dialog.toast({
  104. content: msg,
  105. type: 'error',
  106. })
  107. fileEl.value = ''
  108. }
  109. if (props.accept) {
  110. for (const file of files) {
  111. const accepts = props.accept.split(',').map(atom => {
  112. return atom.trim()
  113. })
  114. const hname = file.name.slice(file.name.lastIndexOf('.')).toLowerCase()
  115. if (!accepts.includes(hname)) {
  116. // return previewError('格式错误', `仅支持${props.accept}格式文件`)
  117. const accept = props.accept.split('.').join('').replace(',', '/').toLowerCase()
  118. return previewError('格式错误', `${t('limit.formFile', { form: accept })}`)
  119. }
  120. }
  121. }
  122. let previews
  123. if (props.preview || normalizeScale.value) {
  124. try {
  125. previews = await producePreviews(files)
  126. } catch (e) {
  127. return previewError(e)
  128. }
  129. }
  130. if (normalizeScale.value) {
  131. const sizesConfirm = []
  132. for (const [i, file] of files.entries()) {
  133. const support = Object.values(supports).find(support => support.types.includes(file.type))
  134. if (support) {
  135. sizesConfirm.push(support.preview(file, previews[i]))
  136. }
  137. }
  138. let sizes
  139. try {
  140. sizes = await Promise.all(sizesConfirm)
  141. } catch (e) {
  142. return previewError(e)
  143. }
  144. for (const [w, h, file] of sizes) {
  145. const scaleDiff = calcScale(...normalizeScale.value) - calcScale(w, h)
  146. if (Math.abs(scaleDiff) > 300) {
  147. // return previewError('error scale', `${file.name}的比例部位不为${props.scale}`)
  148. return previewError('error scale', `${t('limit.scaleFile', { fileName: file.name, scale: props.scale })}`)
  149. }
  150. }
  151. }
  152. if (props.maxSize) {
  153. for (const file of files) {
  154. if (file.size > props.maxSize) {
  155. // return previewError('error size', `${file.name}的大小超过${sizeStr.value}`)
  156. // return previewError('error size', `${file.name}的大小超过${sizeStr.value}`)
  157. const accept = props.accept.split('.').join('').replace(',', '/').toLowerCase()
  158. return previewError('格式错误', `${t('limit.formSize', { size: sizeStr.value, form: accept })}`)
  159. }
  160. }
  161. }
  162. const value = props.modelValue ? (props.multiple ? (toRawType(props.modelValue) === 'Array' ? props.modelValue : [props.modelValue]) : null) : props.multiple ? [] : null
  163. const emitData = props.multiple
  164. ? props.preview
  165. ? [...value, ...files.map((file, i) => ({ file, preview: previews[i] }))]
  166. : [...value, files]
  167. : props.preview
  168. ? { file: files[0], preview: previews[0] }
  169. : files[0]
  170. if (Array.isArray(emitData) && props.maxLen && emitData.length > props.maxLen) {
  171. // return previewError('err len', `最多仅支持${props.maxLen}个文件!`)
  172. return previewError('err len', `${t('limit.maxLengthFile', { length: props.maxLen })}`)
  173. }
  174. emit('update:modelValue', emitData)
  175. fileEl.value = ''
  176. }
  177. defineExpose({
  178. input: inputRef,
  179. })
  180. </script>