Search K
Appearance
Appearance
📊 SEO元描述:2024年最新Vue文件上传教程,详解文件上传组件、拖拽上传、进度显示、文件预览。包含完整上传案例,适合Vue.js开发者掌握文件处理技术。
核心关键词:Vue文件上传 2024、Vue上传组件、文件拖拽上传、Vue文件处理、上传进度显示、文件预览组件
长尾关键词:Vue文件上传怎么实现、Vue拖拽上传组件、文件上传进度条、Vue图片上传预览、文件上传最佳实践
通过本节Vue文件上传处理,你将系统性掌握:
文件上传处理是什么?这是现代Web应用中不可缺少的功能模块。文件上传处理是指在前端实现文件选择、验证、上传和管理的完整流程,也是用户内容管理的重要组成部分。
💡 学习建议:文件上传是前端开发的常见需求,建议从基础上传开始,逐步掌握高级功能
// 🎉 File API基础使用
const fileInput = document.querySelector('input[type="file"]')
fileInput.addEventListener('change', (event) => {
const files = event.target.files
for (let i = 0; i < files.length; i++) {
const file = files[i]
console.log('文件信息:', {
name: file.name, // 文件名
size: file.size, // 文件大小(字节)
type: file.type, // MIME类型
lastModified: file.lastModified // 最后修改时间
})
// 读取文件内容
const reader = new FileReader()
reader.onload = (e) => {
console.log('文件内容:', e.target.result)
}
// 根据文件类型选择读取方式
if (file.type.startsWith('image/')) {
reader.readAsDataURL(file) // 读取为Data URL
} else {
reader.readAsText(file) // 读取为文本
}
}
})// 🎉 使用FormData上传文件
const uploadFile = async (file) => {
const formData = new FormData()
formData.append('file', file)
formData.append('userId', '123')
formData.append('category', 'avatar')
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
// 不要设置Content-Type,让浏览器自动设置
})
if (response.ok) {
const result = await response.json()
console.log('上传成功:', result)
return result
} else {
throw new Error('上传失败')
}
} catch (error) {
console.error('上传错误:', error)
throw error
}
}<!-- 🎉 SimpleUpload.vue - 基础文件上传组件 -->
<template>
<div class="simple-upload">
<div class="upload-area" :class="{ 'is-dragover': isDragOver }">
<!-- 文件输入框 -->
<input
ref="fileInput"
type="file"
:multiple="multiple"
:accept="accept"
class="file-input"
@change="handleFileSelect"
/>
<!-- 上传区域 -->
<div
class="upload-trigger"
@click="triggerFileSelect"
@dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop"
>
<div class="upload-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor">
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
</svg>
</div>
<div class="upload-text">
<p class="primary-text">点击上传文件</p>
<p class="secondary-text">或将文件拖拽到此处</p>
</div>
</div>
</div>
<!-- 文件列表 -->
<div v-if="fileList.length > 0" class="file-list">
<div
v-for="(file, index) in fileList"
:key="file.id"
class="file-item"
:class="{ 'is-uploading': file.status === 'uploading' }"
>
<!-- 文件信息 -->
<div class="file-info">
<div class="file-icon">
<img v-if="file.preview" :src="file.preview" alt="预览" />
<svg v-else width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
</svg>
</div>
<div class="file-details">
<div class="file-name">{{ file.name }}</div>
<div class="file-size">{{ formatFileSize(file.size) }}</div>
</div>
</div>
<!-- 上传进度 -->
<div v-if="file.status === 'uploading'" class="upload-progress">
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: file.progress + '%' }"
></div>
</div>
<span class="progress-text">{{ file.progress }}%</span>
</div>
<!-- 上传状态 -->
<div v-else class="upload-status">
<span v-if="file.status === 'success'" class="status-success">✓</span>
<span v-else-if="file.status === 'error'" class="status-error">✗</span>
<span v-else class="status-waiting">⏳</span>
</div>
<!-- 操作按钮 -->
<div class="file-actions">
<button
v-if="file.status !== 'uploading'"
type="button"
class="action-button remove"
@click="removeFile(index)"
>
删除
</button>
</div>
</div>
</div>
<!-- 上传按钮 -->
<div v-if="hasWaitingFiles" class="upload-actions">
<button
type="button"
class="upload-button"
:disabled="isUploading"
@click="startUpload"
>
{{ isUploading ? '上传中...' : '开始上传' }}
</button>
</div>
</div>
</template>
<script>
import { ref, computed } from 'vue'
export default {
name: 'SimpleUpload',
props: {
// 是否支持多文件
multiple: {
type: Boolean,
default: true
},
// 接受的文件类型
accept: {
type: String,
default: ''
},
// 最大文件大小(字节)
maxSize: {
type: Number,
default: 10 * 1024 * 1024 // 10MB
},
// 最大文件数量
maxCount: {
type: Number,
default: 10
},
// 上传URL
uploadUrl: {
type: String,
default: '/api/upload'
},
// 额外的上传参数
uploadData: {
type: Object,
default: () => ({})
}
},
emits: ['success', 'error', 'progress', 'change'],
setup(props, { emit }) {
const fileInput = ref(null)
const fileList = ref([])
const isDragOver = ref(false)
const isUploading = ref(false)
// 计算属性
const hasWaitingFiles = computed(() => {
return fileList.value.some(file => file.status === 'waiting')
})
// 生成文件ID
const generateFileId = () => {
return Date.now() + Math.random().toString(36).substr(2, 9)
}
// 格式化文件大小
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', '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 validateFile = (file) => {
const errors = []
// 检查文件大小
if (file.size > props.maxSize) {
errors.push(`文件大小不能超过 ${formatFileSize(props.maxSize)}`)
}
// 检查文件类型
if (props.accept && !isFileTypeAccepted(file)) {
errors.push('不支持的文件类型')
}
return errors
}
// 检查文件类型是否被接受
const isFileTypeAccepted = (file) => {
if (!props.accept) return true
const acceptTypes = props.accept.split(',').map(type => type.trim())
return acceptTypes.some(acceptType => {
if (acceptType.startsWith('.')) {
// 扩展名匹配
return file.name.toLowerCase().endsWith(acceptType.toLowerCase())
} else if (acceptType.includes('*')) {
// MIME类型通配符匹配
const regex = new RegExp(acceptType.replace('*', '.*'))
return regex.test(file.type)
} else {
// 精确MIME类型匹配
return file.type === acceptType
}
})
}
// 创建文件预览
const createFilePreview = (file) => {
return new Promise((resolve) => {
if (file.type.startsWith('image/')) {
const reader = new FileReader()
reader.onload = (e) => resolve(e.target.result)
reader.readAsDataURL(file)
} else {
resolve(null)
}
})
}
// 添加文件到列表
const addFilesToList = async (files) => {
for (const file of files) {
// 检查文件数量限制
if (fileList.value.length >= props.maxCount) {
alert(`最多只能上传 ${props.maxCount} 个文件`)
break
}
// 验证文件
const errors = validateFile(file)
if (errors.length > 0) {
alert(errors.join('\n'))
continue
}
// 创建文件对象
const fileObj = {
id: generateFileId(),
name: file.name,
size: file.size,
type: file.type,
file: file,
status: 'waiting',
progress: 0,
preview: await createFilePreview(file)
}
fileList.value.push(fileObj)
}
emit('change', fileList.value)
}
// 触发文件选择
const triggerFileSelect = () => {
fileInput.value?.click()
}
// 处理文件选择
const handleFileSelect = (event) => {
const files = Array.from(event.target.files)
addFilesToList(files)
// 清空input值,允许重复选择同一文件
event.target.value = ''
}
// 处理拖拽悬停
const handleDragOver = (event) => {
isDragOver.value = true
}
// 处理拖拽离开
const handleDragLeave = (event) => {
isDragOver.value = false
}
// 处理文件拖放
const handleDrop = (event) => {
isDragOver.value = false
const files = Array.from(event.dataTransfer.files)
addFilesToList(files)
}
// 移除文件
const removeFile = (index) => {
fileList.value.splice(index, 1)
emit('change', fileList.value)
}
// 上传单个文件
const uploadSingleFile = (fileObj) => {
return new Promise((resolve, reject) => {
const formData = new FormData()
formData.append('file', fileObj.file)
// 添加额外参数
Object.keys(props.uploadData).forEach(key => {
formData.append(key, props.uploadData[key])
})
const xhr = new XMLHttpRequest()
// 监听上传进度
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100)
fileObj.progress = progress
emit('progress', { file: fileObj, progress })
}
})
// 监听上传完成
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText)
fileObj.status = 'success'
fileObj.response = response
emit('success', { file: fileObj, response })
resolve(response)
} catch (error) {
fileObj.status = 'error'
fileObj.error = '响应解析失败'
emit('error', { file: fileObj, error })
reject(error)
}
} else {
fileObj.status = 'error'
fileObj.error = `上传失败: ${xhr.status}`
emit('error', { file: fileObj, error: xhr.statusText })
reject(new Error(xhr.statusText))
}
})
// 监听上传错误
xhr.addEventListener('error', () => {
fileObj.status = 'error'
fileObj.error = '网络错误'
emit('error', { file: fileObj, error: '网络错误' })
reject(new Error('网络错误'))
})
// 开始上传
fileObj.status = 'uploading'
xhr.open('POST', props.uploadUrl)
xhr.send(formData)
})
}
// 开始上传
const startUpload = async () => {
const waitingFiles = fileList.value.filter(file => file.status === 'waiting')
if (waitingFiles.length === 0) return
isUploading.value = true
try {
// 并发上传所有文件
await Promise.allSettled(
waitingFiles.map(file => uploadSingleFile(file))
)
} finally {
isUploading.value = false
}
}
return {
fileInput,
fileList,
isDragOver,
isUploading,
hasWaitingFiles,
formatFileSize,
triggerFileSelect,
handleFileSelect,
handleDragOver,
handleDragLeave,
handleDrop,
removeFile,
startUpload
}
}
}
</script>
<style scoped>
.simple-upload {
width: 100%;
max-width: 600px;
}
.upload-area {
border: 2px dashed #d1d5db;
border-radius: 0.5rem;
padding: 2rem;
text-align: center;
transition: border-color 0.3s ease;
cursor: pointer;
}
.upload-area:hover,
.upload-area.is-dragover {
border-color: #3b82f6;
background-color: #eff6ff;
}
.file-input {
display: none;
}
.upload-trigger {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.upload-icon {
color: #9ca3af;
transition: color 0.3s ease;
}
.upload-area:hover .upload-icon {
color: #3b82f6;
}
.upload-text .primary-text {
font-size: 1.125rem;
font-weight: 500;
color: #374151;
margin: 0 0 0.5rem 0;
}
.upload-text .secondary-text {
font-size: 0.875rem;
color: #6b7280;
margin: 0;
}
.file-list {
margin-top: 1.5rem;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
overflow: hidden;
}
.file-item {
display: flex;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
transition: background-color 0.3s ease;
}
.file-item:last-child {
border-bottom: none;
}
.file-item.is-uploading {
background-color: #eff6ff;
}
.file-info {
display: flex;
align-items: center;
flex: 1;
gap: 0.75rem;
}
.file-icon {
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
background-color: #f3f4f6;
border-radius: 0.375rem;
overflow: hidden;
}
.file-icon img {
width: 100%;
height: 100%;
object-fit: cover;
}
.file-icon svg {
color: #6b7280;
}
.file-details {
flex: 1;
}
.file-name {
font-weight: 500;
color: #374151;
margin-bottom: 0.25rem;
}
.file-size {
font-size: 0.875rem;
color: #6b7280;
}
.upload-progress {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 0 1rem;
}
.progress-bar {
width: 100px;
height: 4px;
background-color: #e5e7eb;
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #3b82f6;
transition: width 0.3s ease;
}
.progress-text {
font-size: 0.875rem;
color: #6b7280;
min-width: 3rem;
}
.upload-status {
margin: 0 1rem;
}
.status-success {
color: #10b981;
font-weight: bold;
}
.status-error {
color: #ef4444;
font-weight: bold;
}
.status-waiting {
color: #f59e0b;
}
.file-actions {
display: flex;
gap: 0.5rem;
}
.action-button {
padding: 0.25rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
background-color: #ffffff;
color: #374151;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.3s ease;
}
.action-button:hover {
background-color: #f9fafb;
}
.action-button.remove {
border-color: #ef4444;
color: #ef4444;
}
.action-button.remove:hover {
background-color: #fef2f2;
}
.upload-actions {
margin-top: 1rem;
text-align: center;
}
.upload-button {
padding: 0.75rem 2rem;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 0.375rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s ease;
}
.upload-button:hover:not(:disabled) {
background-color: #2563eb;
}
.upload-button:disabled {
background-color: #9ca3af;
cursor: not-allowed;
}
</style><!-- 🎉 ImageUpload.vue - 图片上传预览组件 -->
<template>
<div class="image-upload">
<div class="upload-grid">
<!-- 已上传的图片 -->
<div
v-for="(image, index) in imageList"
:key="image.id"
class="image-item"
>
<div class="image-preview">
<img :src="image.url" :alt="image.name" />
<!-- 上传进度遮罩 -->
<div v-if="image.status === 'uploading'" class="upload-overlay">
<div class="progress-circle">
<svg class="progress-ring" width="60" height="60">
<circle
class="progress-ring-circle"
stroke="white"
stroke-width="4"
fill="transparent"
r="26"
cx="30"
cy="30"
:stroke-dasharray="circumference"
:stroke-dashoffset="progressOffset(image.progress)"
/>
</svg>
<span class="progress-text">{{ image.progress }}%</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="image-actions">
<button
type="button"
class="action-btn preview-btn"
@click="previewImage(image)"
>
👁️
</button>
<button
type="button"
class="action-btn delete-btn"
@click="removeImage(index)"
>
🗑️
</button>
</div>
</div>
<div class="image-info">
<div class="image-name">{{ image.name }}</div>
<div class="image-size">{{ formatFileSize(image.size) }}</div>
</div>
</div>
<!-- 上传按钮 -->
<div
v-if="imageList.length < maxCount"
class="upload-slot"
@click="triggerFileSelect"
>
<div class="upload-icon">+</div>
<div class="upload-text">上传图片</div>
</div>
</div>
<!-- 隐藏的文件输入框 -->
<input
ref="fileInput"
type="file"
accept="image/*"
multiple
style="display: none"
@change="handleFileSelect"
/>
<!-- 图片预览模态框 -->
<div v-if="previewVisible" class="preview-modal" @click="closePreview">
<div class="preview-content" @click.stop>
<img :src="previewImage.url" :alt="previewImage.name" />
<button class="close-btn" @click="closePreview">×</button>
</div>
</div>
</div>
</template>
<script>
import { ref, computed } from 'vue'
export default {
name: 'ImageUpload',
props: {
modelValue: {
type: Array,
default: () => []
},
maxCount: {
type: Number,
default: 9
},
maxSize: {
type: Number,
default: 5 * 1024 * 1024 // 5MB
},
uploadUrl: {
type: String,
default: '/api/upload/image'
}
},
emits: ['update:modelValue', 'change', 'success', 'error'],
setup(props, { emit }) {
const fileInput = ref(null)
const imageList = ref([...props.modelValue])
const previewVisible = ref(false)
const previewImage = ref(null)
// 圆形进度条周长
const circumference = 2 * Math.PI * 26
// 计算进度条偏移
const progressOffset = (progress) => {
return circumference - (progress / 100) * circumference
}
// 格式化文件大小
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', '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 triggerFileSelect = () => {
fileInput.value?.click()
}
// 处理文件选择
const handleFileSelect = (event) => {
const files = Array.from(event.target.files)
processFiles(files)
event.target.value = ''
}
// 处理文件
const processFiles = async (files) => {
for (const file of files) {
if (imageList.value.length >= props.maxCount) {
alert(`最多只能上传 ${props.maxCount} 张图片`)
break
}
if (file.size > props.maxSize) {
alert(`图片大小不能超过 ${formatFileSize(props.maxSize)}`)
continue
}
if (!file.type.startsWith('image/')) {
alert('只能上传图片文件')
continue
}
const imageObj = {
id: Date.now() + Math.random().toString(36).substr(2, 9),
name: file.name,
size: file.size,
file: file,
url: await createImagePreview(file),
status: 'waiting',
progress: 0
}
imageList.value.push(imageObj)
uploadImage(imageObj)
}
updateModelValue()
}
// 创建图片预览
const createImagePreview = (file) => {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => resolve(e.target.result)
reader.readAsDataURL(file)
})
}
// 上传图片
const uploadImage = (imageObj) => {
const formData = new FormData()
formData.append('image', imageObj.file)
const xhr = new XMLHttpRequest()
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
imageObj.progress = Math.round((event.loaded / event.total) * 100)
}
})
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText)
imageObj.status = 'success'
imageObj.url = response.url || imageObj.url
imageObj.response = response
emit('success', { image: imageObj, response })
updateModelValue()
} catch (error) {
imageObj.status = 'error'
emit('error', { image: imageObj, error })
}
} else {
imageObj.status = 'error'
emit('error', { image: imageObj, error: xhr.statusText })
}
})
xhr.addEventListener('error', () => {
imageObj.status = 'error'
emit('error', { image: imageObj, error: '网络错误' })
})
imageObj.status = 'uploading'
xhr.open('POST', props.uploadUrl)
xhr.send(formData)
}
// 移除图片
const removeImage = (index) => {
imageList.value.splice(index, 1)
updateModelValue()
}
// 预览图片
const previewImage = (image) => {
previewImage.value = image
previewVisible.value = true
}
// 关闭预览
const closePreview = () => {
previewVisible.value = false
previewImage.value = null
}
// 更新模型值
const updateModelValue = () => {
const successImages = imageList.value.filter(img => img.status === 'success')
emit('update:modelValue', successImages)
emit('change', successImages)
}
return {
fileInput,
imageList,
previewVisible,
previewImage,
circumference,
progressOffset,
formatFileSize,
triggerFileSelect,
handleFileSelect,
removeImage,
previewImage,
closePreview
}
}
}
</script>
<style scoped>
.image-upload {
width: 100%;
}
.upload-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 1rem;
}
.image-item {
position: relative;
}
.image-preview {
position: relative;
width: 120px;
height: 120px;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
overflow: hidden;
cursor: pointer;
transition: transform 0.3s ease;
}
.image-preview:hover {
transform: scale(1.05);
}
.image-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.upload-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.progress-circle {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.progress-ring {
transform: rotate(-90deg);
}
.progress-ring-circle {
transition: stroke-dashoffset 0.3s ease;
}
.progress-text {
position: absolute;
color: white;
font-size: 0.875rem;
font-weight: bold;
}
.image-actions {
position: absolute;
top: 0.5rem;
right: 0.5rem;
display: flex;
gap: 0.25rem;
opacity: 0;
transition: opacity 0.3s ease;
}
.image-preview:hover .image-actions {
opacity: 1;
}
.action-btn {
width: 2rem;
height: 2rem;
border: none;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.6);
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
transition: background-color 0.3s ease;
}
.action-btn:hover {
background-color: rgba(0, 0, 0, 0.8);
}
.image-info {
margin-top: 0.5rem;
text-align: center;
}
.image-name {
font-size: 0.75rem;
color: #374151;
margin-bottom: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.image-size {
font-size: 0.625rem;
color: #9ca3af;
}
.upload-slot {
width: 120px;
height: 120px;
border: 2px dashed #d1d5db;
border-radius: 0.5rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color 0.3s ease, background-color 0.3s ease;
}
.upload-slot:hover {
border-color: #3b82f6;
background-color: #eff6ff;
}
.upload-icon {
font-size: 2rem;
color: #9ca3af;
margin-bottom: 0.5rem;
}
.upload-text {
font-size: 0.875rem;
color: #6b7280;
}
.preview-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.preview-content {
position: relative;
max-width: 90vw;
max-height: 90vh;
}
.preview-content img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.close-btn {
position: absolute;
top: -3rem;
right: 0;
width: 2.5rem;
height: 2.5rem;
border: none;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
color: white;
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
background-color: rgba(255, 255, 255, 0.3);
}
</style>通过本节Vue文件上传处理的学习,你已经掌握:
A: 使用input的accept属性限制文件类型,在JavaScript中检查file.type和file.size进行验证。
A: 使用Blob.slice()方法将文件分片,逐个上传分片,最后在服务器端合并。
A: 使用XMLHttpRequest的upload.progress事件监听上传进度,注意区分上传进度和总体进度。
A: 实现重试机制,记录失败的文件和原因,提供手动重试或自动重试功能。
A: 注意文件大小限制、网络状况、电池消耗,提供压缩选项和离线上传功能。
// 🎉 文件验证最佳实践
const fileValidation = {
// 文件类型验证
validateFileType(file, allowedTypes) {
const fileType = file.type.toLowerCase()
const fileName = file.name.toLowerCase()
return allowedTypes.some(type => {
if (type.startsWith('.')) {
return fileName.endsWith(type)
} else if (type.includes('*')) {
const regex = new RegExp(type.replace('*', '.*'))
return regex.test(fileType)
} else {
return fileType === type
}
})
},
// 文件大小验证
validateFileSize(file, maxSize) {
return file.size <= maxSize
},
// 文件名验证
validateFileName(fileName) {
const invalidChars = /[<>:"/\\|?*]/
return !invalidChars.test(fileName)
}
}// 🎉 上传错误处理
const uploadErrorHandler = {
handleError(error, file) {
const errorMap = {
'NetworkError': '网络连接失败,请检查网络',
'TimeoutError': '上传超时,请重试',
'FileSizeError': '文件大小超出限制',
'FileTypeError': '不支持的文件类型',
'ServerError': '服务器错误,请稍后重试'
}
return errorMap[error.type] || '上传失败,请重试'
},
retryUpload(file, maxRetries = 3) {
let retryCount = 0
const attemptUpload = () => {
return this.uploadFile(file).catch(error => {
retryCount++
if (retryCount < maxRetries) {
console.log(`重试上传 ${file.name},第 ${retryCount} 次`)
return attemptUpload()
} else {
throw error
}
})
}
return attemptUpload()
}
}"文件上传是现代Web应用的重要功能,良好的上传体验能够显著提升用户满意度。继续学习表单性能优化,了解如何优化大型表单的性能和用户体验!"