| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- <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,.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.fileId" 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.fileName }}</span>
- </div>
- <el-icon class="delete-button" @click="removeFile(file.fileId)"><Delete /></el-icon>
- </div>
- </div>
- </div>
- </template>
- <script lang="ts" setup>
- import { ref, computed, nextTick, watch } 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';
- const props = defineProps<{
- label: string;
- fileList?: FileItem[];
- }>();
- // 常量定义
- 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);
- const fileSize = file.size;
- const fileName = file.name;
- fileList.value.unshift({
- fileId: fileId,
- file: file,
- fileType: fileType,
- fileName: fileName,
- fileSize: `${Math.round(fileSize / 1024)}KB`,
- });
- };
- const removeFile = (id: number) => {
- ElMessageBox.confirm('确定删除该文件吗?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- }).then(() => {
- const index = fileList.value.findIndex((item) => item.fileId === id);
- if (index !== -1) {
- fileList.value.splice(index, 1);
- }
- ElMessage.success('文件删除成功');
- });
- };
- const simulateFileUpload = () => {
- // 设置初始进度为5%,让用户立即看到进度
- progress.value = 5;
- // 使用更快的速度模拟上传进度
- let targetProgress = 5;
- const totalDuration = 500; // 总上传时间,毫秒
- 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']);
- watch(
- () => props.fileList,
- (newVal) => {
- fileList.value = newVal || [];
- },
- {
- immediate: true,
- },
- );
- </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;
- width: 100%;
- 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 {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .delete-button {
- color: #ef4444;
- background: none;
- border: none;
- cursor: pointer;
- }
- </style>
|