Upload.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. <template>
  2. <div class="upload-wrapper">
  3. <!-- 上传按钮 -->
  4. <label for="fileInput" class="upload-button">
  5. <el-icon><UploadFilled /></el-icon>
  6. <span>{{ label }}</span>
  7. </label>
  8. <input type="file" id="fileInput" multiple accept=".pdf,.docx,.xls,.xlsx,.ppt,.pptx" @change="handleFileSelect" />
  9. <!-- 总体进度条 -->
  10. <div class="progress-container" v-show="showProgress">
  11. <div class="progress-info">
  12. <span>正在上传文件...</span>
  13. <span>{{ progressPercent }}%</span>
  14. </div>
  15. <div class="progress-bar">
  16. <div
  17. class="progress-bar-fill"
  18. :class="{ completed: isCompleted }"
  19. :style="{ width: `${progressPercent}%` }"
  20. ></div>
  21. </div>
  22. </div>
  23. <!-- 文件列表 -->
  24. <div class="file-list" v-if="fileList.length > 0">
  25. <div v-for="file in fileList" :key="file.fileId" class="file-item">
  26. <div class="file-info">
  27. <img :src="FILE_TYPE_ICON[file.fileType as keyof typeof FILE_TYPE_ICON]" />
  28. <span class="file-name">{{ file.fileName }}</span>
  29. </div>
  30. <el-icon class="delete-button" @click="removeFile(file.fileId)"><Delete /></el-icon>
  31. </div>
  32. </div>
  33. </div>
  34. </template>
  35. <script lang="ts" setup>
  36. import { ref, computed, nextTick, watch } from 'vue';
  37. import { UploadFilled, Delete } from '@element-plus/icons-vue';
  38. import { ElMessage, ElMessageBox } from 'element-plus';
  39. import { FILE_TYPE_ICON } from '../constant';
  40. import type { FileItem } from '../types';
  41. const props = defineProps<{
  42. label: string;
  43. fileList?: FileItem[];
  44. }>();
  45. // 常量定义
  46. const MAX_SIZE = 5 * 1024 * 1024; // 5MB
  47. // 响应式状态
  48. const fileList = ref<FileItem[]>([]);
  49. const tempFiles = ref<File[]>([]);
  50. const showProgress = ref(false);
  51. const isCompleted = ref(false);
  52. const progress = ref(0);
  53. // 计算属性
  54. const progressPercent = computed(() => {
  55. return Math.round(progress.value);
  56. });
  57. // 检查文件是否已存在
  58. const isFileAlreadyUploaded = (newFile: File): boolean => {
  59. return fileList.value.some((item) => {
  60. return item.file?.name === newFile.name && item.file.size === newFile.size && item.file?.type === newFile.type;
  61. });
  62. };
  63. // 获取文件类型
  64. const getFileType = (file: File): string => {
  65. const fileType = file.type;
  66. // 转换为系统中已有的四种类型
  67. if (fileType.includes('pdf')) return 'pdf';
  68. if (fileType.includes('word') || fileType.includes('msword') || fileType.includes('wordprocessingml'))
  69. return 'word';
  70. if (fileType.includes('excel') || fileType.includes('spreadsheetml') || fileType.includes('ms-excel'))
  71. return 'excel';
  72. if (fileType.includes('powerpoint') || fileType.includes('presentationml') || fileType.includes('ms-powerpoint'))
  73. return 'ppt';
  74. // 默认返回
  75. return 'pdf';
  76. };
  77. // 方法
  78. const handleFileSelect = (event: Event) => {
  79. const input = event.target as HTMLInputElement;
  80. if (!input.files || input.files.length === 0) return;
  81. const files = Array.from(input.files);
  82. let validFiles = 0;
  83. tempFiles.value = [];
  84. files.forEach((file) => {
  85. if (!isValidFileType(file)) {
  86. ElMessage.error(`${file.name} 不是允许的文件类型`);
  87. return;
  88. }
  89. if (file.size > MAX_SIZE) {
  90. ElMessage.error(`${file.name} 文件过大`);
  91. return;
  92. }
  93. // 检查是否已经上传过相同的文件
  94. if (isFileAlreadyUploaded(file)) {
  95. ElMessage.warning(`${file.name} 已经上传过了`);
  96. return;
  97. }
  98. // 保存到临时文件数组中,而不是立即添加到UI
  99. tempFiles.value.push(file);
  100. validFiles++;
  101. });
  102. progress.value = 0;
  103. // 显示进度条
  104. if (validFiles > 0) {
  105. showProgress.value = true;
  106. isCompleted.value = false;
  107. // 确保DOM更新后再开始动画
  108. nextTick(() => {
  109. simulateFileUpload();
  110. });
  111. }
  112. // 清空input以便再次选择相同文件
  113. input.value = '';
  114. };
  115. const isValidFileType = (file: File): boolean => {
  116. const allowedTypes = [
  117. 'application/pdf',
  118. 'application/msword',
  119. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  120. 'application/vnd.ms-excel',
  121. 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  122. 'application/vnd.ms-powerpoint',
  123. 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  124. ];
  125. return allowedTypes.includes(file.type);
  126. };
  127. const addFileToUI = (file: File) => {
  128. const fileId = Date.now() + Math.floor(Math.random() * 1000);
  129. const fileType = getFileType(file);
  130. const fileSize = file.size;
  131. const fileName = file.name;
  132. fileList.value.unshift({
  133. fileId: fileId,
  134. file: file,
  135. fileType: fileType,
  136. fileName: fileName,
  137. fileSize: `${Math.round(fileSize / 1024)}KB`,
  138. });
  139. };
  140. const removeFile = (id: number) => {
  141. ElMessageBox.confirm('确定删除该文件吗?', '提示', {
  142. confirmButtonText: '确定',
  143. cancelButtonText: '取消',
  144. }).then(() => {
  145. const index = fileList.value.findIndex((item) => item.fileId === id);
  146. if (index !== -1) {
  147. fileList.value.splice(index, 1);
  148. }
  149. ElMessage.success('文件删除成功');
  150. });
  151. };
  152. const simulateFileUpload = () => {
  153. // 设置初始进度为5%,让用户立即看到进度
  154. progress.value = 5;
  155. // 使用更快的速度模拟上传进度
  156. let targetProgress = 5;
  157. const totalDuration = 500; // 总上传时间,毫秒
  158. const interval = 200; // 更新间隔
  159. const steps = totalDuration / interval;
  160. const increment = 95 / steps; // 每步增加的百分比,从5%到100%
  161. const uploadInterval = setInterval(() => {
  162. targetProgress += increment;
  163. if (targetProgress >= 100) {
  164. progress.value = 100;
  165. clearInterval(uploadInterval);
  166. // 上传完成后再添加文件到UI
  167. setTimeout(() => {
  168. tempFiles.value.forEach((file) => {
  169. addFileToUI(file);
  170. });
  171. isCompleted.value = true;
  172. ElMessage.success('文件上传成功');
  173. emit('uploadSuccess', fileList.value);
  174. setTimeout(() => {
  175. showProgress.value = false;
  176. }, 500);
  177. }, 200);
  178. } else {
  179. progress.value = targetProgress;
  180. }
  181. }, interval);
  182. };
  183. const emit = defineEmits(['uploadSuccess']);
  184. watch(
  185. () => props.fileList,
  186. (newVal) => {
  187. fileList.value = newVal || [];
  188. },
  189. {
  190. immediate: true,
  191. },
  192. );
  193. </script>
  194. <style lang="scss" scoped>
  195. .upload-wrapper {
  196. display: flex;
  197. flex-direction: column;
  198. width: 100%;
  199. }
  200. .upload-button {
  201. @include flex-center;
  202. gap: 5cpx;
  203. width: 100cpx;
  204. border: 1px solid rgba($text-color, 0.15);
  205. color: $primary-color;
  206. font-size: 14cpx;
  207. border-radius: 2px;
  208. text-align: center;
  209. cursor: pointer;
  210. transition: all 0.3s ease-in-out;
  211. &:hover {
  212. background-color: $primary-color;
  213. color: $white-color;
  214. }
  215. }
  216. #fileInput {
  217. display: none;
  218. }
  219. .progress-container {
  220. margin-top: 16cpx;
  221. }
  222. .progress-info {
  223. display: flex;
  224. justify-content: space-between;
  225. font-size: 14cpx;
  226. color: #666;
  227. }
  228. .progress-bar {
  229. width: 100%;
  230. height: 8px;
  231. background-color: #e5e7eb;
  232. border-radius: 9999px;
  233. overflow: hidden;
  234. }
  235. .progress-bar-fill {
  236. height: 100%;
  237. background-color: $primary-color;
  238. border-radius: 9999px;
  239. transition: width 0.3s ease;
  240. &.completed {
  241. background-color: #10b981;
  242. }
  243. }
  244. .file-list {
  245. margin-top: 16cpx;
  246. display: flex;
  247. flex-wrap: wrap;
  248. gap: 8cpx;
  249. }
  250. .file-item {
  251. @include flex-center;
  252. justify-content: space-between;
  253. width: 100%;
  254. border: 1px solid #e5e7eb;
  255. border-radius: 6cpx;
  256. padding: 12cpx;
  257. transition: all 0.2s ease;
  258. &:hover {
  259. background-color: #f8fafc;
  260. }
  261. }
  262. .file-info {
  263. display: flex;
  264. align-items: center;
  265. gap: 8cpx;
  266. img {
  267. width: 20cpx;
  268. }
  269. }
  270. .file-name {
  271. flex: 1;
  272. overflow: hidden;
  273. text-overflow: ellipsis;
  274. white-space: nowrap;
  275. }
  276. .delete-button {
  277. color: #ef4444;
  278. background: none;
  279. border: none;
  280. cursor: pointer;
  281. }
  282. </style>