index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. <template>
  2. <view :class="classes">
  3. <view class="nut-uploader__slot" v-if="$slots.default">
  4. <slot></slot>
  5. <template v-if="maximum - fileList.length">
  6. <input
  7. class="nut-uploader__input"
  8. v-if="capture"
  9. type="file"
  10. capture="camera"
  11. :accept="accept"
  12. :multiple="multiple"
  13. :name="name"
  14. :disabled="disabled"
  15. @change="onChange"
  16. />
  17. <input
  18. class="nut-uploader__input"
  19. v-else
  20. type="file"
  21. :accept="accept"
  22. :multiple="multiple"
  23. :name="name"
  24. :disabled="disabled"
  25. @change="onChange"
  26. />
  27. </template>
  28. </view>
  29. <view class="nut-uploader__preview" :class="[listType]" v-for="(item, index) in fileList" :key="item.uid">
  30. <view class="nut-uploader__preview-img" v-if="listType == 'picture' && !$slots.default">
  31. <view class="nut-uploader__preview__progress" v-if="item.status == 'ready'">
  32. <view class="nut-uploader__preview__progress__msg">{{ item.message }}</view>
  33. </view>
  34. <view class="nut-uploader__preview__progress" v-else-if="item.status != 'success'">
  35. <nut-icon color="#fff" :name="item.status == 'error' ? 'failure' : 'loading'"></nut-icon>
  36. <view class="nut-uploader__preview__progress__msg">{{ item.message }}</view>
  37. </view>
  38. <nut-icon
  39. v-if="isDeletable"
  40. color="rgba(0,0,0,0.6)"
  41. @click="onDelete(item, index)"
  42. class="close"
  43. name="failure"
  44. ></nut-icon>
  45. <img
  46. class="nut-uploader__preview-img__c"
  47. @click="fileItemClick(item)"
  48. v-if="item.type.includes('image') && item.url"
  49. :src="item.url"
  50. />
  51. <view v-else class="nut-uploader__preview-img__file">
  52. <view @click="fileItemClick(item)" class="nut-uploader__preview-img__file__name">
  53. <nut-icon color="#808080" name="link"></nut-icon>&nbsp;{{ item.name }}
  54. </view>
  55. </view>
  56. <view class="tips">{{ item.name }}</view>
  57. </view>
  58. <view class="nut-uploader__preview-list" v-else-if="listType == 'list'">
  59. <view @click="fileItemClick(item)" class="nut-uploader__preview-img__file__name" :class="[item.status]">
  60. <nut-icon name="link" />&nbsp;{{ item.name }}
  61. </view>
  62. <nut-icon
  63. class="nut-uploader__preview-img__file__del"
  64. @click="onDelete(item, index)"
  65. color="#808080"
  66. name="del"
  67. />
  68. <nut-progress
  69. size="small"
  70. :percentage="item.percentage"
  71. v-if="item.status == 'uploading'"
  72. stroke-color="linear-gradient(270deg, rgba(18,126,255,1) 0%,rgba(32,147,255,1) 32.815625%,rgba(13,242,204,1) 100%)"
  73. :show-text="false"
  74. >
  75. </nut-progress>
  76. </view>
  77. </view>
  78. <view
  79. class="nut-uploader__upload"
  80. :class="[listType]"
  81. v-if="listType == 'picture' && !$slots.default && maximum - fileList.length"
  82. >
  83. <nut-icon :size="uploadIconSize" color="#808080" :name="uploadIcon"></nut-icon>
  84. <input
  85. class="nut-uploader__input"
  86. v-if="capture"
  87. type="file"
  88. capture="camera"
  89. :accept="accept"
  90. :multiple="multiple"
  91. :name="name"
  92. :disabled="disabled"
  93. @change="onChange"
  94. />
  95. <input
  96. class="nut-uploader__input"
  97. v-else
  98. type="file"
  99. :accept="accept"
  100. :multiple="multiple"
  101. :name="name"
  102. :disabled="disabled"
  103. @change="onChange"
  104. />
  105. </view>
  106. </view>
  107. </template>
  108. <script lang="ts">
  109. import { computed, reactive } from 'vue';
  110. import { createComponent } from '../../utils/create';
  111. import { Uploader, UploadOptions } from './uploader';
  112. const { componentName, create, translate } = createComponent('uploader');
  113. export type FileItemStatus = 'ready' | 'uploading' | 'success' | 'error';
  114. export class FileItem {
  115. status: FileItemStatus = 'ready';
  116. message: string = translate('ready');
  117. uid: string = new Date().getTime().toString();
  118. name?: string;
  119. url?: string;
  120. type?: string;
  121. percentage: string | number = 0;
  122. formData: FormData = new FormData();
  123. }
  124. export default create({
  125. props: {
  126. name: { type: String, default: 'file' },
  127. url: { type: String, default: '' },
  128. // defaultFileList: { type: Array, default: () => new Array<FileItem>() },
  129. timeout: { type: [Number, String], default: 1000 * 30 },
  130. fileList: { type: Array, default: () => [] },
  131. isPreview: { type: Boolean, default: true },
  132. // picture、list
  133. listType: { type: String, default: 'picture' },
  134. isDeletable: { type: Boolean, default: true },
  135. method: { type: String, default: 'post' },
  136. capture: { type: Boolean, default: false },
  137. maximize: { type: [Number, String], default: Number.MAX_VALUE },
  138. maximum: { type: [Number, String], default: 1 },
  139. clearInput: { type: Boolean, default: true },
  140. accept: { type: String, default: '*' },
  141. headers: { type: Object, default: {} },
  142. data: { type: Object, default: {} },
  143. uploadIcon: { type: String, default: 'photograph' },
  144. uploadIconSize: { type: [String, Number], default: '' },
  145. xhrState: { type: [Number, String], default: 200 },
  146. withCredentials: { type: Boolean, default: false },
  147. multiple: { type: Boolean, default: false },
  148. disabled: { type: Boolean, default: false },
  149. autoUpload: { type: Boolean, default: true },
  150. beforeUpload: {
  151. type: Function,
  152. default: null
  153. },
  154. beforeDelete: {
  155. type: Function,
  156. default: (file: FileItem, files: FileItem[]) => {
  157. return true;
  158. }
  159. },
  160. onChange: { type: Function }
  161. // customRequest: { type: Function }
  162. },
  163. emits: [
  164. 'start',
  165. 'progress',
  166. 'oversize',
  167. 'success',
  168. 'failure',
  169. 'change',
  170. 'delete',
  171. 'update:fileList',
  172. 'file-item-click'
  173. ],
  174. setup(props, { emit }) {
  175. const fileList = reactive(props.fileList) as Array<FileItem>;
  176. let uploadQueue: Promise<Uploader>[] = [];
  177. const classes = computed(() => {
  178. const prefixCls = componentName;
  179. return {
  180. [prefixCls]: true
  181. };
  182. });
  183. const clearInput = (el: HTMLInputElement) => {
  184. el.value = '';
  185. };
  186. const fileItemClick = (fileItem: FileItem) => {
  187. emit('file-item-click', { fileItem });
  188. };
  189. const executeUpload = (fileItem: FileItem, index: number) => {
  190. const uploadOption = new UploadOptions();
  191. uploadOption.url = props.url;
  192. uploadOption.formData = fileItem.formData;
  193. uploadOption.timeout = (props.timeout as number) * 1;
  194. uploadOption.method = props.method;
  195. uploadOption.xhrState = props.xhrState as number;
  196. uploadOption.headers = props.headers;
  197. uploadOption.withCredentials = props.withCredentials;
  198. uploadOption.onStart = (option: UploadOptions) => {
  199. fileItem.status = 'ready';
  200. fileItem.message = translate('readyUpload');
  201. clearUploadQueue(index);
  202. emit('start', option);
  203. };
  204. uploadOption.onProgress = (event: ProgressEvent<XMLHttpRequestEventTarget>, option: UploadOptions) => {
  205. fileItem.status = 'uploading';
  206. fileItem.message = translate('uploading');
  207. fileItem.percentage = ((event.loaded / event.total) * 100).toFixed(0);
  208. emit('progress', { event, option, percentage: fileItem.percentage });
  209. };
  210. uploadOption.onSuccess = (responseText: XMLHttpRequest['responseText'], option: UploadOptions) => {
  211. fileItem.status = 'success';
  212. fileItem.message = translate('success');
  213. emit('success', {
  214. responseText,
  215. option,
  216. fileItem
  217. });
  218. emit('update:fileList', fileList);
  219. };
  220. uploadOption.onFailure = (responseText: XMLHttpRequest['responseText'], option: UploadOptions) => {
  221. fileItem.status = 'error';
  222. fileItem.message = translate('error');
  223. emit('failure', {
  224. responseText,
  225. option,
  226. fileItem
  227. });
  228. };
  229. let task = new Uploader(uploadOption);
  230. if (props.autoUpload) {
  231. task.upload();
  232. } else {
  233. uploadQueue.push(
  234. new Promise((resolve, reject) => {
  235. resolve(task);
  236. })
  237. );
  238. }
  239. };
  240. const clearUploadQueue = (index = -1) => {
  241. if (index > -1) {
  242. uploadQueue.splice(index, 1);
  243. } else {
  244. uploadQueue = [];
  245. }
  246. };
  247. const submit = () => {
  248. Promise.all(uploadQueue).then((res) => {
  249. res.forEach((i) => i.upload());
  250. });
  251. };
  252. const readFile = (files: File[]) => {
  253. files.forEach((file: File, index: number) => {
  254. const formData = new FormData();
  255. for (const [key, value] of Object.entries(props.data)) {
  256. formData.append(key, value);
  257. }
  258. formData.append(props.name, file);
  259. const fileItem = reactive(new FileItem());
  260. fileItem.name = file.name;
  261. fileItem.status = 'ready';
  262. fileItem.type = file.type;
  263. fileItem.formData = formData;
  264. fileItem.message = translate('waitingUpload');
  265. executeUpload(fileItem, index);
  266. if (props.isPreview && file.type.includes('image')) {
  267. const reader = new FileReader();
  268. reader.onload = (event: ProgressEvent<FileReader>) => {
  269. fileItem.url = (event.target as FileReader).result as string;
  270. fileList.push(fileItem);
  271. };
  272. reader.readAsDataURL(file);
  273. } else {
  274. fileList.push(fileItem);
  275. }
  276. });
  277. };
  278. const filterFiles = (files: File[]) => {
  279. const maximum = (props.maximum as number) * 1;
  280. const maximize = (props.maximize as number) * 1;
  281. const oversizes = new Array<File>();
  282. files = files.filter((file: File) => {
  283. if (file.size > maximize) {
  284. oversizes.push(file);
  285. return false;
  286. } else {
  287. return true;
  288. }
  289. });
  290. if (oversizes.length) {
  291. emit('oversize', oversizes);
  292. }
  293. let currentFileLength = files.length + fileList.length;
  294. if (currentFileLength > maximum) {
  295. files.splice(files.length - (currentFileLength - maximum));
  296. }
  297. return files;
  298. };
  299. const onDelete = (file: FileItem, index: number) => {
  300. clearUploadQueue(index);
  301. if (props.beforeDelete(file, fileList)) {
  302. fileList.splice(index, 1);
  303. emit('delete', {
  304. file,
  305. fileList,
  306. index
  307. });
  308. } else {
  309. // console.log('用户阻止了删除!');
  310. }
  311. };
  312. const onChange = (event: InputEvent) => {
  313. if (props.disabled) {
  314. return;
  315. }
  316. const $el = event.target as HTMLInputElement;
  317. let { files } = $el;
  318. if (props.beforeUpload) {
  319. props.beforeUpload(files).then((f: Array<File>) => {
  320. const _files: File[] = filterFiles(new Array<File>().slice.call(f));
  321. readFile(_files);
  322. });
  323. } else {
  324. const _files: File[] = filterFiles(new Array<File>().slice.call(files));
  325. readFile(_files);
  326. }
  327. emit('change', {
  328. fileList,
  329. event
  330. });
  331. if (props.clearInput) {
  332. clearInput($el);
  333. }
  334. };
  335. return {
  336. onChange,
  337. onDelete,
  338. fileList,
  339. classes,
  340. fileItemClick,
  341. clearUploadQueue,
  342. submit
  343. };
  344. }
  345. });
  346. </script>