Vue3 + TypeScript 实现文件拖拽上传
·
应用效果:



实例代码:CommonApplyBasicInfoForm.vue
<script setup lang="ts" name="CommonApplyBasicInfoForm">
......
// 选择文件列表
const selectedFiles = ref<FileList | null>(null);
// 通过 FormData 对象实现文件上传
let formData = new FormData();
// 受理文件列表
const acceptFiles = ref<ApplyBasicFileVO[]>([]);
// 拖放事件处理函数,拖放文件,轻开鼠标左键触发
const onDrop = async (e: DragEvent) => {
// 获取文件对象列表(拖放的文件列表)
if (e.dataTransfer) selectedFiles.value = e.dataTransfer.files;
// 判断文件列表是否为空
if (!selectedFiles.value?.length) {
ElMessage.warning("请选择至少一个文件");
return;
}
try {
// 清空 FormData 表单数据的内容:重新赋值,创建新实例,旧数据被丢弃(完全清空),需要使用 let 声明对象,不能使用 const 声明对象
formData = new FormData();
// 添加多个文件到 FormData
for (const file of selectedFiles.value) {
formData.append("uploadFiles", file);
}
// 添加受理编号到 FormData
formData.append("outerApplyId", applyBasicInfo.value.outerApplyId);
// 发送请求,上传多个文件到后端服务器
await applyBasicInfoUploadFilesService(formData);
ElMessage.success("文件上传成功!");
// 获取已上传的文件列表
await fetchUploadedFiles(applyBasicInfo.value.outerApplyId);
} catch (error) {
ElMessage.error("文件上传失败!");
}
};
// 获取已上传的文件列表
const fetchUploadedFiles = async (outerApplyId: string) => {
const result = await applyBasicInfoQueryFilesService(outerApplyId);
acceptFiles.value = result.data;
};
......
</script>
<template>
......
<!-- 拖拽上传区域 -->
<!-- @drop.prevent="onDrop":监听拖拽放置事件,阻止默认行为并调用onDrop方法 -->
<!-- @dragover.prevent:监听拖拽悬停事件并阻止默认行为(某些浏览器默认打开拖放的文件),也可以写为 @dragover.prevent="" 或 @dragover.prevent = "dragover = true" 或 @dragover.prevent = "method" -->
<!-- @dragleave.prevent:监听拖拽离开事件并阻止默认行为,也可以写为 @dragleave.prevent="" 或 @dragleave.prevent = "dragover = false" 或 @dragleave.prevent = "method" -->
<!-- @dragleave="":监听拖拽离开事件,也可以写成 @dragleave="false" 或 @dragleave="dragover = false" 或 @dragleave="method"-->
<div class="drag-drop-area" @drop.prevent="onDrop" @dragover.prevent>
<div class="drag-icon">
<!-- <i class="fas fa-cloud-upload-alt"></i> -->
<el-icon color="#4a6bdf" size="60"><MostlyCloudy /></el-icon>
</div>
<h3>拖放文件到此处</h3>
<p>或者点击选择文件</p>
</div>
......
</template>
<style scoped lang="scss">
......
.drag-drop-area {
flex: 1;
border: 2px dashed #4a6bdf;
border-radius: 12px;
padding: 10px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.drag-drop-area:hover{
// transform 对元素进行2D或3D变换,常用于微调元素位置或解决某些浏览器渲染问题,属于CSS3变换属性,不会影响文档流和其他元素布局
// translateX(1px) 表示在X轴方向上平移1像素,正值表示向下移动,负值表示向上移动
// translateY(1px) 表示在Y轴方向上平移1像素,正值表示向右移动,负值表示向左移动
transform: translateY(-1px);
background: #f0f4ff;
}
.drag-icon {
font-size: 48px;
color: #4a6bdf;
margin-bottom: 15px;
}
......
</style>
TypeScript :拖放事件处理函数
typescript
// 定义事件处理函数类型
type DropEventHandler = (event: DragEvent) => void;
// 假设这是在Vue 3 Composition API环境中的代码
// 定义响应式变量类型
interface ReactiveState {
dragover: { value: boolean };
}
// 定义文件处理函数类型
type AddFilesFunction = (files: File[]) => void;
// 完整的onDrop函数实现
const onDrop: DropEventHandler = (event: DragEvent): void => {
// 确保事件对象存在
if (!event) {
console.error('Drop event is undefined or null');
return;
}
// 确保dataTransfer对象存在
if (!event.dataTransfer) {
console.error('Data transfer is not available in this event');
return;
}
// 防止默认行为(某些浏览器可能会尝试打开拖放的文件)
event.preventDefault();
// 更新响应式状态(假设这是在Vue组件中)
// 这里假设dragover是一个ref对象,包含value属性
dragover.value = false;
try {
// 获取拖放的文件列表并转换为数组
const droppedFiles: File[] = Array.from(event.dataTransfer.files);
// 验证是否确实有文件
if (droppedFiles.length === 0) {
console.warn('No files were dropped');
return;
}
// 调用处理函数
addFiles(droppedFiles);
} catch (error) {
console.error('Error processing dropped files:', error);
}
};
// 辅助函数:验证文件类型
const isValidFileType = (file: File, allowedTypes?: string[]): boolean => {
if (!allowedTypes || allowedTypes.length === 0) {
return true; // 如果没有限制,所有文件类型都有效
}
return allowedTypes.some(type => {
// 支持通配符,如 "image/*"
if (type.endsWith('/*')) {
const category = type.split('/')[0];
return file.type.startsWith(`${category}/`);
}
return file.type === type;
});
};
// 辅助函数:验证文件大小
const isValidFileSize = (file: File, maxSize?: number): boolean => {
if (!maxSize) {
return true; // 如果没有大小限制,所有文件都有效
}
return file.size <= maxSize;
};
// 增强版的addFiles函数(带验证)
const addFiles: AddFilesFunction = (files: File[]): void => {
// 定义允许的文件类型和最大文件大小
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf', 'text/plain'];
const maxFileSize = 10 * 1024 * 1024; // 10MB
// 过滤有效文件
const validFiles = files.filter(file =>
isValidFileType(file, allowedTypes) &&
isValidFileSize(file, maxFileSize)
);
// 处理无效文件
const invalidFiles = files.filter(file =>
!isValidFileType(file, allowedTypes) ||
!isValidFileSize(file, maxFileSize)
);
// 记录或处理无效文件
if (invalidFiles.length > 0) {
console.warn(`${invalidFiles.length} files were rejected due to type or size restrictions`);
// 可以在这里显示用户通知
invalidFiles.forEach(file => {
if (!isValidFileType(file, allowedTypes)) {
console.warn(`File ${file.name} has an invalid type: ${file.type}`);
} else if (!isValidFileSize(file, maxFileSize)) {
console.warn(`File ${file.name} exceeds the size limit: ${formatFileSize(file.size)}`);
}
});
}
// 如果有有效文件,继续处理
if (validFiles.length > 0) {
// 实际的文件处理逻辑
console.log(`Processing ${validFiles.length} valid files`);
// 这里可以调用上传函数或其他处理逻辑
// uploadFiles(validFiles);
}
};
// 格式化文件大小的辅助函数
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// 如果需要,可以添加事件监听器的设置函数
const setupDropZone = (element: HTMLElement): void => {
// 防止默认的拖放行为
const preventDefault = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
// 设置事件监听器
element.addEventListener('dragenter', preventDefault);
element.addEventListener('dragover', preventDefault);
element.addEventListener('dragleave', preventDefault);
element.addEventListener('drop', onDrop);
// 添加视觉反馈
element.addEventListener('dragenter', () => {
dragover.value = true;
element.classList.add('drag-over');
});
element.addEventListener('dragleave', () => {
dragover.value = false;
element.classList.remove('drag-over');
});
element.addEventListener('drop', () => {
dragover.value = false;
element.classList.remove('drag-over');
});
};
// 假设的Vue 3组件中使用示例
/*
import { ref } from 'vue';
export default {
setup() {
const dragover = ref(false);
// 在模板渲染后设置拖放区域
onMounted(() => {
const dropZone = document.getElementById('drop-zone');
if (dropZone) {
setupDropZone(dropZone);
}
});
return {
dragover
};
}
};
*/
// 导出函数和类型(如果使用模块系统)
export type { DropEventHandler, AddFilesFunction };
export { onDrop, addFiles, setupDropZone, isValidFileType, isValidFileSize, formatFileSize };
关键改进点
-
类型注解:
-
为事件参数
event添加了DragEvent类型 -
为函数添加了明确的返回类型 (
void) -
定义了自定义类型
DropEventHandler和AddFilesFunction
-
-
错误处理:
-
添加了空值检查,确保事件对象和
dataTransfer存在 -
使用 try-catch 块处理可能的异常
-
添加了文件验证逻辑
-
-
文件验证:
-
添加了文件类型验证函数
isValidFileType -
添加了文件大小验证函数
isValidFileSize -
在
addFiles函数中过滤无效文件
-
-
辅助功能:
-
添加了文件大小格式化函数
formatFileSize -
添加了拖放区域设置函数
setupDropZone -
提供了完整的视觉反馈处理
-
-
代码健壮性:
-
添加了详细的警告和错误日志
-
提供了更完整的用户体验(如视觉反馈)
-
考虑了边界情况(如空文件列表)
-
使用说明
这个 TypeScript 实现不仅转换了原始的 JavaScript 代码,还增加了类型安全性和错误处理。在实际项目中,您可能需要根据具体框架(如 Vue、React 或 Angular)调整实现方式。
如果您是在 Vue 3 中使用这个函数,可能需要将 dragover.value 替换为适当的响应式引用,并根据组件结构调整代码。
更多推荐


所有评论(0)