index.vue 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. <template>
  2. <div>
  3. <div class="flex-box">
  4. <van-uploader
  5. multiple
  6. :accept="fileType.toString()"
  7. :max-count="limit"
  8. :max-size="fileSize * 1024 * 1024"
  9. @oversize="onOversize"
  10. :before-read="beforeRead"
  11. :after-read="afterRead">
  12. <van-button icon="plus" type="primary" class="button">上传文件</van-button>
  13. </van-uploader>
  14. <slot></slot>
  15. </div>
  16. <div v-if="isShowTip" class="upload-tip">
  17. <div v-if="fileType">1、支持{{ fileType.join('、') }}文件</div>
  18. <div v-if="limit">2、最多支持上传{{limit}}个文件</div>
  19. </div>
  20. <!-- 文件列表 -->
  21. <transition-group class="upload-file-list" name="el-fade-in-linear" tag="ul">
  22. <li v-for="(file, index) in fileList" :key="file.uid" class="upload-list-item">
  23. <div class="text-primary" @click="handleDownload(file)">
  24. {{ getFileName(file.name) }}
  25. </div>
  26. <van-icon name="delete-o" class="delete-icon" @click="handleDelete(index)"/>
  27. </li>
  28. </transition-group>
  29. </div>
  30. </template>
  31. <script lang="ts" setup name="FileUpload">
  32. import {showFailToast, showToast} from "vant";
  33. import {ref} from "vue";
  34. import { v1 as uuidv1 } from 'uuid';
  35. import {download2, globalHeaders} from "@/utils/request";
  36. import axios from "axios";
  37. const props = defineProps({
  38. modelValue: {
  39. type: [String, Object, Array],
  40. default: () => []
  41. },
  42. buttonText: {
  43. type: String,
  44. default: '选取文件'
  45. },
  46. // 数量限制
  47. limit: {
  48. type: Number,
  49. default: 5
  50. },
  51. // 大小限制(MB)
  52. fileSize: {
  53. type: Number,
  54. default: 100
  55. },
  56. // 文件类型, 例如['png', 'jpg', 'jpeg']
  57. fileType: {
  58. type: Array,
  59. default: ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'pdf']
  60. },
  61. // 是否显示提示
  62. isShowTip: {
  63. type: Boolean,
  64. default: true
  65. }
  66. });
  67. const emit = defineEmits(['update:modelValue']);
  68. const number = ref(0);
  69. const uploadList = ref<any[]>([]);
  70. const baseUrl = import.meta.env.VITE_BASE_API;
  71. const downLoadApi = import.meta.env.VITE_BASE_DOWNLOAD_API;
  72. const uploadFileUrl = ref(baseUrl + '/resource/oss/upload'); // 上传文件服务器地址
  73. const headers = ref(globalHeaders());
  74. const fileList = ref<any[]>([]);
  75. const onOversize = () => {
  76. showToast('文件大小不能超过' + props.fileSize + 'MB');
  77. };
  78. // 上传前置处理
  79. const beforeRead = (file: any) => {
  80. // 校检文件类型
  81. if (props.fileType.length) {
  82. const fileName = file.name.split('.');
  83. const fileExt = fileName[fileName.length - 1];
  84. const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
  85. if (!isTypeOk) {
  86. showToast(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`);
  87. return false;
  88. }
  89. }
  90. // 校检文件大小
  91. if (props.fileSize) {
  92. const isLt = file.size / 1024 / 1024 < props.fileSize;
  93. if (!isLt) {
  94. showToast(`上传文件大小不能超过 ${props.fileSize} MB!`);
  95. return false;
  96. }
  97. }
  98. number.value++;
  99. return true;
  100. };
  101. // 上传
  102. const afterRead = async (file: any) => {
  103. let url = baseUrl + '/file/upload/uploadfile'; //上传文件接口
  104. try {
  105. // 如果文件大于等于5MB,分片上传
  106. const res = await uploadByPieces(url, file);
  107. // 分片上传后操作
  108. if (res.code === 200) {
  109. uploadList.value.push({
  110. name: res.originalName,
  111. url: res.filename
  112. });
  113. uploadedSuccessfully();
  114. } else {
  115. number.value--;
  116. showFailToast(res.msg)
  117. // fileUploadRef.value?.handleRemove(file);
  118. uploadedSuccessfully();
  119. }
  120. } catch (e) {
  121. return e;
  122. }
  123. };
  124. //分片上传
  125. const uploadByPieces = async (url, { file }) => {
  126. // 上传过程中用到的变量
  127. const chunkSize = 5 * 1024 * 1024; // 5MB一片
  128. const chunkCount = Math.ceil(file.size / chunkSize); // 总片数
  129. // 获取当前chunk数据
  130. const identifier = uuidv1();
  131. const getChunkInfo = (file, index) => {
  132. let start = index * chunkSize;
  133. let end = Math.min(file.size, start + chunkSize);
  134. let chunknumber = file.slice(start, end);
  135. const fileName = file.name;
  136. return { chunknumber, fileName };
  137. };
  138. // 分片上传接口
  139. const uploadChunk = (data, params) => {
  140. return new Promise((resolve, reject) => {
  141. axios({
  142. url,
  143. method: 'post',
  144. data,
  145. params,
  146. headers: {
  147. 'Content-Type': 'multipart/form-data'
  148. }
  149. })
  150. .then((res) => {
  151. return resolve(res.data);
  152. })
  153. .catch((err) => {
  154. return reject(err);
  155. });
  156. });
  157. };
  158. // 针对单个文件进行chunk上传
  159. const readChunk = (index) => {
  160. const { chunknumber } = getChunkInfo(file, index);
  161. let fetchForm = new FormData();
  162. fetchForm.append('file', chunknumber);
  163. return uploadChunk(fetchForm, {
  164. chunknumber: index,
  165. identifier: identifier
  166. });
  167. };
  168. // 针对每个文件进行chunk处理
  169. const promiseList = [];
  170. try {
  171. for (let index = 0; index < chunkCount; ++index) {
  172. promiseList.push(readChunk(index));
  173. }
  174. await Promise.all(promiseList);
  175. // 文件分片上传完成后,调用合并接口
  176. const res = await mergeChunks(identifier, file.name);
  177. res.originalName = file.name;
  178. return res;
  179. } catch (e) {
  180. return e;
  181. }
  182. };
  183. const mergeChunks = async (identifier: string, filename: string) => {
  184. try {
  185. const response = await axios.post(baseUrl + '/file/upload/mergefile', null, {
  186. params: {
  187. identifier: identifier,
  188. filename: filename,
  189. chunkstar: 0 // 假设所有分片的开始序号为0
  190. }
  191. });
  192. return response.data;
  193. } catch (error) {
  194. throw new Error('合并请求失败');
  195. }
  196. };
  197. // 上传结束处理
  198. const uploadedSuccessfully = () => {
  199. if (number.value > 0 && uploadList.value.length === number.value) {
  200. fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
  201. uploadList.value = [];
  202. number.value = 0;
  203. emit('update:modelValue', fileList.value);
  204. }
  205. };
  206. // 删除文件
  207. const handleDelete = (index: number) => {
  208. fileList.value.splice(index, 1);
  209. emit('update:modelValue', fileList.value);
  210. };
  211. // 获取文件名称
  212. const getFileName = (name: string) => {
  213. // 如果是url那么取最后的名字 如果不是直接返回
  214. if (name.lastIndexOf('/') > -1) {
  215. return name.slice(name.lastIndexOf('/') + 1);
  216. } else {
  217. return name;
  218. }
  219. };
  220. // 下载方法
  221. const handleDownload = (file: any) => {
  222. download2(baseUrl + downLoadApi + file.url, file.name);
  223. };
  224. </script>
  225. <style lang="scss" scoped>
  226. .button {
  227. padding: 0 10px;
  228. height: 26px;
  229. background: #2C81FF;
  230. border-radius: 2px;
  231. margin-bottom: 6px;
  232. }
  233. .upload-tip {
  234. font-size: 14px;
  235. }
  236. .upload-file-list {
  237. .upload-list-item {
  238. display: flex;
  239. align-items: center;
  240. .text-primary {
  241. color: #2c81ff;
  242. font-size: 14px;
  243. }
  244. .delete-icon {
  245. color: #969799;
  246. font-size: 20px;
  247. margin-left: 5px;
  248. }
  249. }
  250. }
  251. .flex-box {
  252. display: flex;
  253. align-items: center;
  254. }
  255. </style>