index.vue 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. <template>
  2. <view :class="classes">
  3. <view class="preview" v-for="item in fileList" :key="item.uid">
  4. <view class="preview-img">
  5. <nut-icon
  6. v-if="isDeletable"
  7. color="rgba(0,0,0,0.6)"
  8. @click="onDelete(item, index)"
  9. class="close"
  10. name="mask-close"
  11. ></nut-icon>
  12. <img v-if="item.type.includes('image') && item.url" :src="item.url" />
  13. <view class="tips" v-if="item.status != 'success'">{{
  14. item.status
  15. }}</view>
  16. </view>
  17. </view>
  18. <view class="upload" v-if="maxCount - fileList.length">
  19. <nut-icon color="#808080" :name="uploadIcon"></nut-icon>
  20. <input
  21. type="file"
  22. :capture="capture"
  23. :accept="acceptType"
  24. :multiple="multiple"
  25. :name="name"
  26. :disabled="disabled"
  27. @change="onChange"
  28. />
  29. </view>
  30. </view>
  31. </template>
  32. <script lang="ts">
  33. import { computed, reactive } from 'vue';
  34. import { createComponent } from '@/utils/create';
  35. import { Uploader, UploadOptions } from './uploader';
  36. const { componentName, create } = createComponent('uploader');
  37. export type FileItemStatus =
  38. | 'ready'
  39. | 'uploading'
  40. | 'success'
  41. | 'error'
  42. | 'removed';
  43. export class FileItem {
  44. status: FileItemStatus = 'ready';
  45. uid: string = new Date().getTime().toString();
  46. name?: string;
  47. url?: string;
  48. type?: string;
  49. formData: FormData = new FormData();
  50. }
  51. export default create({
  52. props: {
  53. name: { type: String, default: 'file' },
  54. url: { type: String, default: '' },
  55. // defaultFileList: { type: Array, default: () => new Array<FileItem>() },
  56. fileList: { type: Array, default: () => [] },
  57. isPreview: { type: Boolean, default: true },
  58. isDeletable: { type: Boolean, default: true },
  59. method: { type: String, default: 'post' },
  60. capture: { type: String, default: 'camera' },
  61. maxSize: { type: [Number, String], default: Number.MAX_VALUE },
  62. maxCount: { type: [Number, String], default: 1 },
  63. clearInput: { type: Boolean, default: false },
  64. acceptType: { type: String, default: '*' },
  65. headers: { type: Object, default: {} },
  66. formData: { type: Object, default: {} },
  67. uploadIcon: { type: String, default: 'photograph' },
  68. xhrState: { type: [Number, String], default: 200 },
  69. withCredentials: { type: Boolean, default: false },
  70. multiple: { type: Boolean, default: false },
  71. disabled: { type: Boolean, default: false },
  72. beforeUpload: {
  73. type: Function,
  74. default: null
  75. },
  76. beforeDelete: {
  77. type: Function,
  78. default: (file: FileItem, files: FileItem[]) => {
  79. return true;
  80. }
  81. },
  82. onChange: { type: Function }
  83. // customRequest: { type: Function }
  84. },
  85. emits: [
  86. 'start',
  87. 'progress',
  88. 'oversize',
  89. 'success',
  90. 'failure',
  91. 'change',
  92. 'delete'
  93. ],
  94. setup(props, { emit }) {
  95. const fileList = reactive(props.fileList) as Array<FileItem>;
  96. const classes = computed(() => {
  97. const prefixCls = componentName;
  98. return {
  99. [prefixCls]: true
  100. };
  101. });
  102. const clearInput = (el: HTMLInputElement) => {
  103. el.value = '';
  104. };
  105. const executeUpload = (fileItem: FileItem) => {
  106. const uploadOption = new UploadOptions();
  107. uploadOption.url = props.url;
  108. for (const [key, value] of Object.entries(props.formData)) {
  109. fileItem.formData.append(key, value);
  110. }
  111. uploadOption.formData = fileItem.formData;
  112. uploadOption.method = props.method;
  113. uploadOption.xhrState = props.xhrState as number;
  114. uploadOption.headers = props.headers;
  115. uploadOption.withCredentials = props.withCredentials;
  116. uploadOption.onStart = (option: UploadOptions) => {
  117. fileItem.status = 'ready';
  118. emit('start', option);
  119. };
  120. uploadOption.onProgress = (
  121. e: ProgressEvent<XMLHttpRequestEventTarget>,
  122. option: UploadOptions
  123. ) => {
  124. fileItem.status = 'uploading';
  125. emit('progress', { e, option });
  126. };
  127. uploadOption.onSuccess = (
  128. responseText: XMLHttpRequest['responseText'],
  129. option: UploadOptions
  130. ) => {
  131. fileItem.status = 'success';
  132. emit('success', {
  133. responseText,
  134. option
  135. });
  136. };
  137. uploadOption.onFailure = (
  138. responseText: XMLHttpRequest['responseText'],
  139. option: UploadOptions
  140. ) => {
  141. fileItem.status = 'error';
  142. emit('failure', {
  143. responseText,
  144. option
  145. });
  146. };
  147. new Uploader(uploadOption).upload();
  148. };
  149. const readFile = (files: File[]) => {
  150. files.forEach((file: File) => {
  151. const formData = new FormData();
  152. formData.append(props.name, file);
  153. const fileItem = new FileItem();
  154. fileItem.name = file.name;
  155. fileItem.status = 'uploading';
  156. fileItem.type = file.type;
  157. fileItem.formData = formData;
  158. executeUpload(fileItem);
  159. if (props.isPreview && file.type.includes('image')) {
  160. const reader = new FileReader();
  161. reader.onload = (event: ProgressEvent<FileReader>) => {
  162. fileItem.url = (event.target as FileReader).result as string;
  163. fileList.push(fileItem);
  164. };
  165. reader.readAsDataURL(file);
  166. } else {
  167. fileList.push(fileItem);
  168. }
  169. });
  170. };
  171. const filterFiles = (files: File[]) => {
  172. const maxCount = (props.maxCount as number) * 1;
  173. const maxSize = (props.maxSize as number) * 1;
  174. const oversizes = new Array<File>();
  175. files = files.filter((file: File) => {
  176. if (file.size > maxSize) {
  177. oversizes.push(file);
  178. return false;
  179. } else {
  180. return true;
  181. }
  182. });
  183. if (oversizes.length) {
  184. emit('oversize', oversizes);
  185. }
  186. if (files.length > maxCount) {
  187. files.splice(maxCount - 1, files.length - maxCount);
  188. }
  189. return files;
  190. };
  191. const onDelete = (file: FileItem, index: number) => {
  192. if (props.beforeDelete(file, fileList)) {
  193. fileList.splice(index, 1);
  194. emit('delete', {
  195. file,
  196. fileList
  197. });
  198. } else {
  199. console.log('用户阻止了删除!');
  200. }
  201. };
  202. const onChange = (event: InputEvent) => {
  203. if (props.disabled) {
  204. return;
  205. }
  206. const $el = event.target as HTMLInputElement;
  207. let { files } = $el;
  208. if (props.clearInput) {
  209. clearInput($el);
  210. }
  211. if (props.beforeUpload) {
  212. props.beforeUpload(files).then((f: Array<File>) => {
  213. const _files: File[] = filterFiles(new Array<File>().slice.call(f));
  214. readFile(_files);
  215. });
  216. } else {
  217. const _files: File[] = filterFiles(new Array<File>().slice.call(files));
  218. readFile(_files);
  219. }
  220. emit('change', {
  221. fileList,
  222. event
  223. });
  224. };
  225. return {
  226. onChange,
  227. onDelete,
  228. fileList,
  229. classes
  230. };
  231. }
  232. });
  233. </script>
  234. <style lang="scss">
  235. @import 'index.scss';
  236. </style>