<template>
<div class="upload-component">
<div class="upload-header">
<h3>文件上传</h3>
<div class="upload-stats" v-if="stats.total > 0">
已上传: {{ stats.successful }}/{{ stats.total }}
失败: {{ stats.failed }}
</div>
</div>
<div
class="upload-dropzone"
:class="{ 'is-dragover': isDragging }"
@dragenter.prevent="isDragging = true"
@dragover.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
@drop.prevent="handleDrop"
@click="$refs.fileInput.click()"
>
<input
type="file"
ref="fileInput"
multiple
:accept="acceptedTypes"
@change="handleFileSelect"
class="hidden"
/>
<div class="upload-prompt">
<i class="upload-icon"></i>
<p>拖拽文件到此处或点击选择文件</p>
<p class="upload-hint">
支持的文件类型: {{ supportedTypes.join(', ') }}
<br>
单文件最大: {{ formatSize(maxFileSize) }}
</p>
</div>
</div>
<div v-if="### 9.2 完整的前端组件示例(续)
```vue
<div v-if="files.length" class="file-list">
<div
v-for="(file, index) in files"
:key="file.id"
class="file-item"
:class="{ 'is-error': file.error }"
>
<div class="file-info">
<span class="file-name">{{ file.name }}</span>
<span class="file-size">{{ formatSize(file.size) }}</span>
<span class="file-type">{{ file.type }}</span>
</div>
<div class="file-status">
<template v-if="file.status === 'pending'">
<button
@click="uploadFile(file)"
class="upload-btn"
>
上传
</button>
<button
@click="removeFile(index)"
class="remove-btn"
>
删除
</button>
</template>
<template v-else-if="file.status === 'uploading'">
<div class="progress-bar">
<div
class="progress"
:style="{ width: file.progress + '%' }"
></div>
<span class="progress-text">
{{ file.progress }}%
</span>
</div>
<button
@click="cancelUpload(file)"
class="cancel-btn"
>
取消
</button>
</template>
<template v-else-if="file.status === 'success'">
<span class="success-text">上传成功</span>
</template>
<template v-else-if="file.status === 'error'">
<span class="error-text">{{ file.error }}</span>
<button
@click="retryUpload(file)"
class="retry-btn"
>
重试
</button>
</template>
</div>
</div>
</div>
<div class="upload-actions" v-if="hasPendingFiles">
<button
@click="uploadAllFiles"
:disabled="uploading"
class="upload-all-btn"
>
{{ uploading ? '上传中...' : '上传全部' }}
</button>
</div>
<div class="upload-tips">
<h4>上传提示:</h4>
<ul>
<li>支持拖拽上传或点击选择文件</li>
<li>单个文件大小不超过{{ formatSize(maxFileSize) }}</li>
<li>支持批量上传,单次最多{{ maxFiles }}个文件</li>
<li>上传失败的文件可以重试</li>
<li>上传过程中可以取消</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'FileUploader',
props: {
// 支持的文件类型
acceptedTypes: {
type: String,
default: '*/*'
},
// 最大文件大小(字节)
maxFileSize: {
type: Number,
default: 10 * 1024 * 1024 // 10MB
},
// 最大文件数
maxFiles: {
type: Number,
default: 10
},
// 自动上传
autoUpload: {
type: Boolean,
default: false
}
},
data() {
return {
files: [],
isDragging: false,
uploading: false,
stats: {
total: 0,
successful: 0,
failed: 0
}
}
},
computed: {
hasPendingFiles() {
return this.files.some(f => f.status === 'pending')
},
supportedTypes() {
return this.acceptedTypes.split(',').map(t => t.trim())
}
},
methods: {
// 处理文件选择
handleFileSelect(event) {
const selectedFiles = Array.from(event.target.files)
this.addFiles(selectedFiles)
event.target.value = null // 重置input,允许选择相同文件
},
// 处理拖放
handleDrop(event) {
this.isDragging = false
const droppedFiles = Array.from(event.dataTransfer.files)
this.addFiles(droppedFiles)
},
// 添加文件
addFiles(newFiles) {
// 验证文件
const validFiles = newFiles.filter(file => {
if (file.size > this.maxFileSize) {
this.showError(`文件 ${file.name} 超过大小限制`)
return false
}
if (!this.isTypeAllowed(file.type)) {
this.showError(`不支持的文件类型:${file.type}`)
return false
}
return true
})
// 检查文件数量限制
if (this.files.length + validFiles.length > this.maxFiles) {
this.showError(`最多只能上传 ${this.maxFiles} 个文件`)
return
}
// 添加文件到列表
validFiles.forEach(file => {
this.files.push({
id: Date.now() + Math.random(),
file,
name: file.name,
size: file.size,
type: file.type,
status: 'pending',
progress: 0,
error: null
})
})
// 自动上传
if (this.autoUpload) {
this.uploadAllFiles()
}
},
// 上传单个文件
async uploadFile(file) {
if (file.status === 'uploading') return
file.status = 'uploading'
file.error = null
this.stats.total++
try {
const formData = new FormData()
formData.append('file', file.file)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
onUploadProgress: (e) => {
file.progress = Math.round((e.loaded * 100) / e.total)
}
})
if (!response.ok) {
throw new Error('上传失败')
}
const result = await response.json()
if (result.code !== 0) {
throw new Error(result.message || '上传失败')
}
file.status = 'success'
this.stats.successful++
this.$emit('upload-success', file, result.data)
} catch (error) {
file.status = 'error'
file.error = error.message
this.stats.failed++
this.$emit('upload-error', file, error)
}
},
// 上传所有文件
async uploadAllFiles() {
this.uploading = true
const pending = this.files.filter(f => f.status === 'pending')
try {
await Promise.all(pending.map(file => this.uploadFile(file)))
} finally {
this.uploading = false
}
},
// 重试上传
retryUpload(file) {
file.status = 'pending'
file.progress = 0
file.error = null
this.uploadFile(file)
},
// 取消上传
cancelUpload(file) {
// TODO: 实现取消上传逻辑
file.status = 'pending'
file.progress = 0
},
// 移除文件
removeFile(index) {
this.files.splice(index, 1)
},
// 工具方法
isTypeAllowed(type) {
return this.acceptedTypes === '*/*' ||
this.supportedTypes.some(t => type.match(new RegExp(t.replace('*', '.*'))))
},
formatSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB']
let size = bytes
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}
return `${size.toFixed(2)} ${units[unitIndex]}`
},
showError(message) {
this.$emit('error', message)
// 如果使用element-ui等UI库
// this.$message.error(message)
}
}
}
</script>
<style scoped>
.upload-component {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.upload-dropzone {
border: 2px dashed #ccc;
border-radius: 4px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.upload-dropzone.is-dragover {
background-color: #f8f9fa;
border-color: #409eff;
}
.file-list {
margin-top: 20px;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
margin: 5px 0;
background: #f8f9fa;
border-radius: 4px;
}
.file-item.is-error {
background: #fff2f0;
}
.progress-bar {
width: 200px;
height: 6px;
background: #eee;
border-radius: 3px;
overflow: hidden;
}
.progress {
height: 100%;
background: #409eff;
transition: width 0.3s ease;
}
/* 其他样式... */
</style>