index.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. <template>
  2. <a-upload
  3. :action="uploadUrl"
  4. :headers="headers"
  5. :accept="accept"
  6. :data="uploadDataDict"
  7. :show-upload-list="false"
  8. :custom-request="customRequest"
  9. :disabled="disabled || loading"
  10. :before-upload="handleBeforeUpload"
  11. @change="handleFileChange"
  12. >
  13. <slot>
  14. <a-button>
  15. <template #icon><ImportOutlined /></template>点击导入
  16. </a-button>
  17. </slot>
  18. </a-upload>
  19. </template>
  20. <script setup lang="ts">
  21. import { computed, ref, watch } from "vue";
  22. import { DownloadOutlined, ImportOutlined } from "@ant-design/icons-vue";
  23. import { message } from "ant-design-vue";
  24. import type { UploadProps } from "ant-design-vue";
  25. import type { AxiosError, AxiosProgressEvent } from "axios";
  26. import { request } from "@/utils/request";
  27. import { getFileMD5 } from "@/utils/crypto";
  28. defineOptions({
  29. name: "ImportBtn",
  30. });
  31. const props = withDefaults(
  32. defineProps<{
  33. uploadUrl: string;
  34. format?: string[];
  35. uploadData?: Record<string, any>;
  36. maxSize?: number;
  37. disabled?: boolean;
  38. uploadFileAlias?: string;
  39. }>(),
  40. {
  41. uploadUrl: "",
  42. format: () => ["xls", "xlsx"],
  43. uploadData: () => {
  44. return {};
  45. },
  46. maxSize: 20 * 1024 * 1024,
  47. uploadFileAlias: "file",
  48. disabled: false,
  49. }
  50. );
  51. // interface ResultData {
  52. // success: boolean;
  53. // message: string;
  54. // }
  55. // type UploadSuccessData = ResultData & UploadResultType;
  56. // const emit = defineEmits<{
  57. // uploading: [];
  58. // uploadError: [value: ResultData];
  59. // uploadSuccess: [value: UploadSuccessData];
  60. // validError: [value: ResultData];
  61. // }>();
  62. const emit = defineEmits([
  63. "uploading",
  64. "uploadError",
  65. "uploadSuccess",
  66. "validError",
  67. ]);
  68. const uploadRef = ref();
  69. const canUpload = ref(false);
  70. const uploadDataDict = ref({});
  71. const headers = ref({ md5: "" });
  72. const result = ref({ success: true, message: "" });
  73. const loading = ref(false);
  74. const uploadProgress = ref(0);
  75. const curFileUid = ref("");
  76. const accept = computed(() => {
  77. return props.format.map((el) => `.${el}`).join();
  78. });
  79. function checkFileFormat(fileType: string) {
  80. const fileFormat = fileType.split(".").pop()?.toLocaleLowerCase();
  81. return props.format.some((item) => item.toLocaleLowerCase() === fileFormat);
  82. }
  83. const handleFileChange: UploadProps["onChange"] = ({ file, fileList }) => {
  84. if (!fileList.length) {
  85. curFileUid.value = "";
  86. result.value = {
  87. success: true,
  88. message: "",
  89. };
  90. return;
  91. }
  92. if (curFileUid.value !== file.uid) {
  93. result.value = {
  94. success: true,
  95. message: "",
  96. };
  97. }
  98. curFileUid.value = file.uid;
  99. canUpload.value = file.status === "uploading";
  100. };
  101. const handleBeforeUpload: UploadProps["beforeUpload"] = async (file) => {
  102. uploadDataDict.value = {
  103. ...props.uploadData,
  104. filename: file.name,
  105. };
  106. if (file.size > props.maxSize) {
  107. handleExceededSize();
  108. return Promise.reject(result.value);
  109. }
  110. if (!checkFileFormat(file.name)) {
  111. handleFormatError();
  112. return Promise.reject(result.value);
  113. }
  114. const md5 = await getFileMD5(file);
  115. headers.value.md5 = md5;
  116. loading.value = true;
  117. };
  118. interface UploadResultType {
  119. hasError: boolean;
  120. failRecords: Array<{
  121. msg: string;
  122. lineNum: number;
  123. }>;
  124. }
  125. const customRequest: UploadProps["customRequest"] = (option) => {
  126. const { file, data } = option;
  127. const formData = new FormData();
  128. const paramData: Record<string, any> = data || {};
  129. Object.entries(paramData).forEach(([k, v]) => {
  130. formData.append(k, v);
  131. });
  132. formData.append(props.uploadFileAlias, file as File);
  133. emit("uploading");
  134. (
  135. request({
  136. url: option.action as string,
  137. data: formData,
  138. headers: option.headers,
  139. onUploadProgress: (data: AxiosProgressEvent) => {
  140. uploadProgress.value = data.total
  141. ? Math.floor((100 * data.loaded) / data.total)
  142. : 0;
  143. },
  144. }) as Promise<UploadResultType>
  145. )
  146. .then((res) => {
  147. // 所有excel导入的特殊处理
  148. if (res.hasError) {
  149. const failRecords = res.failRecords;
  150. const message = failRecords
  151. .map((item) => `第${item.lineNum}行:${item.msg}`)
  152. .join("。");
  153. handleError(message);
  154. return;
  155. }
  156. handleSuccess(res);
  157. })
  158. .catch((error: AxiosError<{ message: string }> | null) => {
  159. handleError(error?.response?.data?.message);
  160. });
  161. };
  162. function handleError(message: string | undefined) {
  163. canUpload.value = false;
  164. loading.value = false;
  165. result.value = {
  166. success: false,
  167. message: message || "上传错误",
  168. };
  169. emit("uploadError", result.value);
  170. }
  171. function handleSuccess(data: UploadResultType) {
  172. canUpload.value = false;
  173. loading.value = false;
  174. result.value = {
  175. success: true,
  176. message: "上传成功!",
  177. };
  178. emit("uploadSuccess", {
  179. ...result.value,
  180. data,
  181. });
  182. }
  183. function handleFormatError() {
  184. const content = `只支持文件格式为${props.format.join("/")}`;
  185. result.value = {
  186. success: false,
  187. message: content,
  188. };
  189. loading.value = false;
  190. emit("validError", result.value);
  191. }
  192. function handleExceededSize() {
  193. const content = `文件大小不能超过${Math.floor(props.maxSize / 1024)}M`;
  194. result.value = {
  195. success: false,
  196. message: content,
  197. };
  198. loading.value = false;
  199. emit("validError", result.value);
  200. }
  201. </script>