Uploader.mjs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import { ref, reactive, defineComponent, onBeforeUnmount, nextTick, mergeProps as _mergeProps, createVNode as _createVNode, vShow as _vShow, withDirectives as _withDirectives } from "vue";
  2. import { pick, extend, toArray, isPromise, truthProp, getSizeStyle, makeArrayProp, makeStringProp, makeNumericProp } from "../utils/index.mjs";
  3. import { bem, name, isOversize, filterFiles, isImageFile, readFileContent } from "./utils.mjs";
  4. import { useCustomFieldValue } from "@vant/use";
  5. import { useExpose } from "../composables/use-expose.mjs";
  6. import { Icon } from "../icon/index.mjs";
  7. import { showImagePreview } from "../image-preview/index.mjs";
  8. import UploaderPreviewItem from "./UploaderPreviewItem.mjs";
  9. const uploaderProps = {
  10. name: makeNumericProp(""),
  11. accept: makeStringProp("image/*"),
  12. capture: String,
  13. multiple: Boolean,
  14. disabled: Boolean,
  15. readonly: Boolean,
  16. lazyLoad: Boolean,
  17. maxCount: makeNumericProp(Infinity),
  18. imageFit: makeStringProp("cover"),
  19. resultType: makeStringProp("dataUrl"),
  20. uploadIcon: makeStringProp("photograph"),
  21. uploadText: String,
  22. deletable: truthProp,
  23. reupload: Boolean,
  24. afterRead: Function,
  25. showUpload: truthProp,
  26. modelValue: makeArrayProp(),
  27. beforeRead: Function,
  28. beforeDelete: Function,
  29. previewSize: [Number, String, Array],
  30. previewImage: truthProp,
  31. previewOptions: Object,
  32. previewFullImage: truthProp,
  33. maxSize: {
  34. type: [Number, String, Function],
  35. default: Infinity
  36. }
  37. };
  38. var stdin_default = defineComponent({
  39. name,
  40. props: uploaderProps,
  41. emits: ["delete", "oversize", "clickUpload", "closePreview", "clickPreview", "clickReupload", "update:modelValue"],
  42. setup(props, {
  43. emit,
  44. slots
  45. }) {
  46. const inputRef = ref();
  47. const urls = [];
  48. const reuploadIndex = ref(-1);
  49. const isReuploading = ref(false);
  50. const getDetail = (index = props.modelValue.length) => ({
  51. name: props.name,
  52. index
  53. });
  54. const resetInput = () => {
  55. if (inputRef.value) {
  56. inputRef.value.value = "";
  57. }
  58. };
  59. const onAfterRead = (items) => {
  60. resetInput();
  61. if (isOversize(items, props.maxSize)) {
  62. if (Array.isArray(items)) {
  63. const result = filterFiles(items, props.maxSize);
  64. items = result.valid;
  65. emit("oversize", result.invalid, getDetail());
  66. if (!items.length) {
  67. return;
  68. }
  69. } else {
  70. emit("oversize", items, getDetail());
  71. return;
  72. }
  73. }
  74. items = reactive(items);
  75. if (reuploadIndex.value > -1) {
  76. const arr = [...props.modelValue];
  77. arr.splice(reuploadIndex.value, 1, items);
  78. emit("update:modelValue", arr);
  79. reuploadIndex.value = -1;
  80. } else {
  81. emit("update:modelValue", [...props.modelValue, ...toArray(items)]);
  82. }
  83. if (props.afterRead) {
  84. props.afterRead(items, getDetail());
  85. }
  86. };
  87. const readFile = (files) => {
  88. const {
  89. maxCount,
  90. modelValue,
  91. resultType
  92. } = props;
  93. if (Array.isArray(files)) {
  94. const remainCount = +maxCount - modelValue.length;
  95. if (files.length > remainCount) {
  96. files = files.slice(0, remainCount);
  97. }
  98. Promise.all(files.map((file) => readFileContent(file, resultType))).then((contents) => {
  99. const fileList = files.map((file, index) => {
  100. const result = {
  101. file,
  102. status: "",
  103. message: "",
  104. objectUrl: URL.createObjectURL(file)
  105. };
  106. if (contents[index]) {
  107. result.content = contents[index];
  108. }
  109. return result;
  110. });
  111. onAfterRead(fileList);
  112. });
  113. } else {
  114. readFileContent(files, resultType).then((content) => {
  115. const result = {
  116. file: files,
  117. status: "",
  118. message: "",
  119. objectUrl: URL.createObjectURL(files)
  120. };
  121. if (content) {
  122. result.content = content;
  123. }
  124. onAfterRead(result);
  125. });
  126. }
  127. };
  128. const onChange = (event) => {
  129. const {
  130. files
  131. } = event.target;
  132. if (props.disabled || !files || !files.length) {
  133. return;
  134. }
  135. const file = files.length === 1 ? files[0] : [].slice.call(files);
  136. if (props.beforeRead) {
  137. const response = props.beforeRead(file, getDetail());
  138. if (!response) {
  139. resetInput();
  140. return;
  141. }
  142. if (isPromise(response)) {
  143. response.then((data) => {
  144. if (data) {
  145. readFile(data);
  146. } else {
  147. readFile(file);
  148. }
  149. }).catch(resetInput);
  150. return;
  151. }
  152. }
  153. readFile(file);
  154. };
  155. let imagePreview;
  156. const onClosePreview = () => emit("closePreview");
  157. const previewImage = (item) => {
  158. if (props.previewFullImage) {
  159. const imageFiles = props.modelValue.filter(isImageFile);
  160. const images = imageFiles.map((item2) => {
  161. if (item2.objectUrl && !item2.url && item2.status !== "failed") {
  162. item2.url = item2.objectUrl;
  163. urls.push(item2.url);
  164. }
  165. return item2.url;
  166. }).filter(Boolean);
  167. imagePreview = showImagePreview(extend({
  168. images,
  169. startPosition: imageFiles.indexOf(item),
  170. onClose: onClosePreview
  171. }, props.previewOptions));
  172. }
  173. };
  174. const closeImagePreview = () => {
  175. if (imagePreview) {
  176. imagePreview.close();
  177. }
  178. };
  179. const deleteFile = (item, index) => {
  180. const fileList = props.modelValue.slice(0);
  181. fileList.splice(index, 1);
  182. emit("update:modelValue", fileList);
  183. emit("delete", item, getDetail(index));
  184. };
  185. const reuploadFile = (index) => {
  186. isReuploading.value = true;
  187. reuploadIndex.value = index;
  188. nextTick(() => chooseFile());
  189. };
  190. const onInputClick = () => {
  191. if (!isReuploading.value) {
  192. reuploadIndex.value = -1;
  193. }
  194. isReuploading.value = false;
  195. };
  196. const renderPreviewItem = (item, index) => {
  197. const needPickData = ["imageFit", "deletable", "reupload", "previewSize", "beforeDelete"];
  198. const previewData = extend(pick(props, needPickData), pick(item, needPickData, true));
  199. return _createVNode(UploaderPreviewItem, _mergeProps({
  200. "item": item,
  201. "index": index,
  202. "onClick": () => emit(props.reupload ? "clickReupload" : "clickPreview", item, getDetail(index)),
  203. "onDelete": () => deleteFile(item, index),
  204. "onPreview": () => previewImage(item),
  205. "onReupload": () => reuploadFile(index)
  206. }, pick(props, ["name", "lazyLoad"]), previewData), pick(slots, ["preview-cover", "preview-delete"]));
  207. };
  208. const renderPreviewList = () => {
  209. if (props.previewImage) {
  210. return props.modelValue.map(renderPreviewItem);
  211. }
  212. };
  213. const onClickUpload = (event) => emit("clickUpload", event);
  214. const renderUpload = () => {
  215. const lessThanMax = props.modelValue.length < +props.maxCount;
  216. const Input = props.readonly ? null : _createVNode("input", {
  217. "ref": inputRef,
  218. "type": "file",
  219. "class": bem("input"),
  220. "accept": props.accept,
  221. "capture": props.capture,
  222. "multiple": props.multiple && reuploadIndex.value === -1,
  223. "disabled": props.disabled,
  224. "onChange": onChange,
  225. "onClick": onInputClick
  226. }, null);
  227. if (slots.default) {
  228. return _withDirectives(_createVNode("div", {
  229. "class": bem("input-wrapper"),
  230. "onClick": onClickUpload
  231. }, [slots.default(), Input]), [[_vShow, lessThanMax]]);
  232. }
  233. return _withDirectives(_createVNode("div", {
  234. "class": bem("upload", {
  235. readonly: props.readonly
  236. }),
  237. "style": getSizeStyle(props.previewSize),
  238. "onClick": onClickUpload
  239. }, [_createVNode(Icon, {
  240. "name": props.uploadIcon,
  241. "class": bem("upload-icon")
  242. }, null), props.uploadText && _createVNode("span", {
  243. "class": bem("upload-text")
  244. }, [props.uploadText]), Input]), [[_vShow, props.showUpload && lessThanMax]]);
  245. };
  246. const chooseFile = () => {
  247. if (inputRef.value && !props.disabled) {
  248. inputRef.value.click();
  249. }
  250. };
  251. onBeforeUnmount(() => {
  252. urls.forEach((url) => URL.revokeObjectURL(url));
  253. });
  254. useExpose({
  255. chooseFile,
  256. reuploadFile,
  257. closeImagePreview
  258. });
  259. useCustomFieldValue(() => props.modelValue);
  260. return () => _createVNode("div", {
  261. "class": bem()
  262. }, [_createVNode("div", {
  263. "class": bem("wrapper", {
  264. disabled: props.disabled
  265. })
  266. }, [renderPreviewList(), renderUpload()])]);
  267. }
  268. });
  269. export {
  270. stdin_default as default,
  271. uploaderProps
  272. };