|
|
@@ -0,0 +1,320 @@
|
|
|
+<template>
|
|
|
+ <div class="upload-wrapper">
|
|
|
+ <!-- 上传按钮 -->
|
|
|
+ <label for="fileInput" class="upload-button">
|
|
|
+ <el-icon><UploadFilled /></el-icon>
|
|
|
+ <span>{{ label }}</span>
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ type="file"
|
|
|
+ id="fileInput"
|
|
|
+ multiple
|
|
|
+ accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
|
|
|
+ @change="handleFileSelect"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 总体进度条 -->
|
|
|
+ <div class="progress-container" v-show="showProgress">
|
|
|
+ <div class="progress-info">
|
|
|
+ <span>正在上传文件...</span>
|
|
|
+ <span>{{ progressPercent }}%</span>
|
|
|
+ </div>
|
|
|
+ <div class="progress-bar">
|
|
|
+ <div
|
|
|
+ class="progress-bar-fill"
|
|
|
+ :class="{ completed: isCompleted }"
|
|
|
+ :style="{ width: `${progressPercent}%` }"
|
|
|
+ ></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 文件列表 -->
|
|
|
+ <div class="file-list" v-if="fileList.length > 0">
|
|
|
+ <div v-for="file in fileList" :key="file.id" class="file-item">
|
|
|
+ <div class="file-info">
|
|
|
+ <img :src="FILE_TYPE_ICON[file.fileType as keyof typeof FILE_TYPE_ICON]" />
|
|
|
+ <span class="file-name">{{ file.file.name }}</span>
|
|
|
+ </div>
|
|
|
+ <button class="delete-button" @click="removeFile(file.id)">
|
|
|
+ <el-icon><Delete /></el-icon>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup>
|
|
|
+ import { ref, computed, nextTick } from 'vue';
|
|
|
+ import { UploadFilled, Delete } from '@element-plus/icons-vue';
|
|
|
+ import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
+ import { FILE_TYPE_ICON } from '../constant';
|
|
|
+ import type { FileItem } from '../types';
|
|
|
+ defineProps<{
|
|
|
+ label: string;
|
|
|
+ }>();
|
|
|
+
|
|
|
+ // 常量定义
|
|
|
+ const MAX_SIZE = 5 * 1024 * 1024; // 5MB
|
|
|
+
|
|
|
+ // 响应式状态
|
|
|
+ const fileList = ref<FileItem[]>([]);
|
|
|
+ const tempFiles = ref<File[]>([]);
|
|
|
+ const showProgress = ref(false);
|
|
|
+ const isCompleted = ref(false);
|
|
|
+ const progress = ref(0);
|
|
|
+
|
|
|
+ // 计算属性
|
|
|
+ const progressPercent = computed(() => {
|
|
|
+ return Math.round(progress.value);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 检查文件是否已存在
|
|
|
+ const isFileAlreadyUploaded = (newFile: File): boolean => {
|
|
|
+ return fileList.value.some((item) => {
|
|
|
+ return item.file.name === newFile.name && item.file.size === newFile.size && item.file.type === newFile.type;
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // 获取文件类型
|
|
|
+ const getFileType = (file: File): string => {
|
|
|
+ const fileType = file.type;
|
|
|
+
|
|
|
+ // 转换为系统中已有的四种类型
|
|
|
+ if (fileType.includes('pdf')) return 'pdf';
|
|
|
+ if (fileType.includes('word') || fileType.includes('msword') || fileType.includes('wordprocessingml'))
|
|
|
+ return 'word';
|
|
|
+ if (fileType.includes('excel') || fileType.includes('spreadsheetml') || fileType.includes('ms-excel'))
|
|
|
+ return 'excel';
|
|
|
+ if (fileType.includes('powerpoint') || fileType.includes('presentationml') || fileType.includes('ms-powerpoint'))
|
|
|
+ return 'ppt';
|
|
|
+
|
|
|
+ // 默认返回
|
|
|
+ return 'pdf';
|
|
|
+ };
|
|
|
+
|
|
|
+ // 方法
|
|
|
+ const handleFileSelect = (event: Event) => {
|
|
|
+ const input = event.target as HTMLInputElement;
|
|
|
+ if (!input.files || input.files.length === 0) return;
|
|
|
+
|
|
|
+ const files = Array.from(input.files);
|
|
|
+ let validFiles = 0;
|
|
|
+ tempFiles.value = [];
|
|
|
+
|
|
|
+ files.forEach((file) => {
|
|
|
+ if (!isValidFileType(file)) {
|
|
|
+ ElMessage.error(`${file.name} 不是允许的文件类型`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (file.size > MAX_SIZE) {
|
|
|
+ ElMessage.error(`${file.name} 文件过大`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否已经上传过相同的文件
|
|
|
+ if (isFileAlreadyUploaded(file)) {
|
|
|
+ ElMessage.warning(`${file.name} 已经上传过了`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存到临时文件数组中,而不是立即添加到UI
|
|
|
+ tempFiles.value.push(file);
|
|
|
+ validFiles++;
|
|
|
+ });
|
|
|
+
|
|
|
+ progress.value = 0;
|
|
|
+
|
|
|
+ // 显示进度条
|
|
|
+ if (validFiles > 0) {
|
|
|
+ showProgress.value = true;
|
|
|
+ isCompleted.value = false;
|
|
|
+
|
|
|
+ // 确保DOM更新后再开始动画
|
|
|
+ nextTick(() => {
|
|
|
+ simulateFileUpload();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清空input以便再次选择相同文件
|
|
|
+ input.value = '';
|
|
|
+ };
|
|
|
+
|
|
|
+ const isValidFileType = (file: File): boolean => {
|
|
|
+ const allowedTypes = [
|
|
|
+ 'application/pdf',
|
|
|
+ 'application/msword',
|
|
|
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
|
+ 'application/vnd.ms-excel',
|
|
|
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
|
+ 'application/vnd.ms-powerpoint',
|
|
|
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
|
+ ];
|
|
|
+ return allowedTypes.includes(file.type);
|
|
|
+ };
|
|
|
+
|
|
|
+ const addFileToUI = (file: File) => {
|
|
|
+ const fileId = Date.now() + Math.floor(Math.random() * 1000);
|
|
|
+ const fileType = getFileType(file);
|
|
|
+
|
|
|
+ fileList.value.unshift({
|
|
|
+ id: fileId,
|
|
|
+ file: file,
|
|
|
+ fileType: fileType,
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const removeFile = (id: number) => {
|
|
|
+ ElMessageBox.confirm('确定删除该文件吗?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ }).then(() => {
|
|
|
+ const index = fileList.value.findIndex((item) => item.id === id);
|
|
|
+ if (index !== -1) {
|
|
|
+ fileList.value.splice(index, 1);
|
|
|
+ }
|
|
|
+ ElMessage.success('文件删除成功');
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const simulateFileUpload = () => {
|
|
|
+ // 设置初始进度为5%,让用户立即看到进度
|
|
|
+ progress.value = 5;
|
|
|
+
|
|
|
+ // 使用更快的速度模拟上传进度
|
|
|
+ let targetProgress = 5;
|
|
|
+ const totalDuration = 1500; // 总上传时间,毫秒
|
|
|
+ const interval = 200; // 更新间隔
|
|
|
+ const steps = totalDuration / interval;
|
|
|
+ const increment = 95 / steps; // 每步增加的百分比,从5%到100%
|
|
|
+
|
|
|
+ const uploadInterval = setInterval(() => {
|
|
|
+ targetProgress += increment;
|
|
|
+
|
|
|
+ if (targetProgress >= 100) {
|
|
|
+ progress.value = 100;
|
|
|
+ clearInterval(uploadInterval);
|
|
|
+
|
|
|
+ // 上传完成后再添加文件到UI
|
|
|
+ setTimeout(() => {
|
|
|
+ tempFiles.value.forEach((file) => {
|
|
|
+ addFileToUI(file);
|
|
|
+ });
|
|
|
+
|
|
|
+ isCompleted.value = true;
|
|
|
+ ElMessage.success('文件上传成功');
|
|
|
+ emit('uploadSuccess', fileList.value);
|
|
|
+ setTimeout(() => {
|
|
|
+ showProgress.value = false;
|
|
|
+ }, 500);
|
|
|
+ }, 200);
|
|
|
+ } else {
|
|
|
+ progress.value = targetProgress;
|
|
|
+ }
|
|
|
+ }, interval);
|
|
|
+ };
|
|
|
+ const emit = defineEmits(['uploadSuccess']);
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+ .upload-wrapper {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-button {
|
|
|
+ @include flex-center;
|
|
|
+ gap: 5cpx;
|
|
|
+ width: 100cpx;
|
|
|
+ border: 1px solid rgba($text-color, 0.15);
|
|
|
+ color: $primary-color;
|
|
|
+ font-size: 14cpx;
|
|
|
+ border-radius: 2px;
|
|
|
+ text-align: center;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s ease-in-out;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: $primary-color;
|
|
|
+ color: $white-color;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #fileInput {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .progress-container {
|
|
|
+ margin-top: 16cpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .progress-info {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-size: 14cpx;
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+
|
|
|
+ .progress-bar {
|
|
|
+ width: 100%;
|
|
|
+ height: 8px;
|
|
|
+ background-color: #e5e7eb;
|
|
|
+ border-radius: 9999px;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .progress-bar-fill {
|
|
|
+ height: 100%;
|
|
|
+ background-color: $primary-color;
|
|
|
+ border-radius: 9999px;
|
|
|
+ transition: width 0.3s ease;
|
|
|
+
|
|
|
+ &.completed {
|
|
|
+ background-color: #10b981;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-list {
|
|
|
+ margin-top: 16cpx;
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8cpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-item {
|
|
|
+ @include flex-center;
|
|
|
+ justify-content: space-between;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 6cpx;
|
|
|
+ padding: 12cpx;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: #f8fafc;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8cpx;
|
|
|
+ img {
|
|
|
+ width: 20cpx;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-name {
|
|
|
+ width: 200cpx;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .delete-button {
|
|
|
+ color: #ef4444;
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+</style>
|