schema 用vue3 setup封装一个上传图片的组件
时间: 2025-07-22 16:41:19 浏览: 2
<think>我们将在Vue3的setup语法中封装一个图片上传组件,该组件将支持以下功能:
1. 支持单图/多图上传
2. 支持图片裁剪(可选)
3. 支持图片压缩
4. 支持上传进度显示
5. 支持限制文件类型和大小
6. 集成到表单schema中
组件名:SchemaImageUpload
我们将使用以下技术栈:
- Vue3 Composition API
- 可能依赖第三方库:如axios用于上传,compressorjs用于图片压缩(可选),但为了简洁,我们将使用原生方式实现压缩和上传
由于问题要求封装一个组件,我们将提供组件的代码框架,并给出关键实现。
注意:为了简化,裁剪功能我们使用一个假设的裁剪组件,或者使用第三方裁剪库(如cropperjs)的封装,但这里我们主要关注组件的接口设计,裁剪功能将作为可选功能。
组件的props设计:
根据之前schema配置的需求,我们设计以下props:
1. modelValue: 用于v-model绑定,可以是字符串(单图)或数组(多图)
2. action: 上传的URL
3. maxCount: 最大上传数量,默认为1(单图),大于1则为多图
4. accept: 接受的文件类型,默认为'image/*'
5. maxSize: 最大文件大小(MB),默认2MB
6. crop: 是否启用裁剪,默认为false
7. aspectRatio: 裁剪的宽高比,如16/9
8. compress: 压缩配置,如{ quality: 0.8, maxWidth: 1920 }
组件事件:
1. update:modelValue: 更新绑定的值(图片URL或URL数组)
2. on-success: 单文件上传成功事件
3. on-error: 上传错误事件
4. on-exceed: 文件超出限制事件
实现思路:
1. 使用input[type=file]选择文件
2. 选择文件后进行验证(类型、大小)
3. 如果启用压缩,则进行客户端压缩
4. 如果启用裁剪,则打开裁剪模态框,裁剪后再上传
5. 上传文件到服务器,获取返回的URL
6. 根据maxCount更新modelValue(单图则替换,多图则追加)
7. 显示上传进度和结果
由于裁剪功能需要模态框和交互,我们将裁剪功能作为独立组件,并在图片上传组件内按条件渲染。
由于代码较长,我们将分步骤实现,并提供关键代码片段。
注意:为了简化示例,我们不会实现完整的裁剪功能,而是用伪代码表示裁剪流程。
组件代码结构:</think>### Vue3 Setup 封装图片上传组件方案
#### 核心组件实现
```vue
<template>
<div class="image-uploader">
<input
type="file"
ref="fileInput"
accept="image/*"
@change="handleFileChange"
hidden
>
<!-- 上传区域 -->
<div v-if="!previewUrl" class="upload-area" @click="triggerFileInput">
<slot name="upload-area">
<div class="placeholder">
<PlusOutlined />
<p>点击上传图片</p>
</div>
</slot>
</div>
<!-- 预览区域 -->
<div v-else class="preview-area">
<img :src="previewUrl" alt="预览图" />
<div class="actions">
<ReloadOutlined @click="reupload" />
<DeleteOutlined @click="removeImage" />
</div>
</div>
<!-- 进度条 -->
<div v-if="uploading" class="progress">
<a-progress :percent="progressPercent" status="active" />
</div>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue';
import { PlusOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons-vue';
const props = defineProps({
modelValue: [String, Array], // 支持单图/多图
action: {
type: String,
required: true
},
maxCount: {
type: Number,
default: 1
},
maxSize: {
type: Number,
default: 5 // MB
},
accept: {
type: String,
default: 'image/*'
},
crop: Boolean,
aspectRatio: Number
});
const emit = defineEmits(['update:modelValue', 'upload-success', 'upload-error']);
const fileInput = ref(null);
const previewUrl = ref('');
const uploading = ref(false);
const progressPercent = ref(0);
// 初始化预览
watch(() => props.modelValue, (val) => {
if (val) {
previewUrl.value = Array.isArray(val) ? val[0] : val;
}
}, { immediate: true });
// 触发文件选择
const triggerFileInput = () => {
fileInput.value.click();
};
// 处理文件选择
const handleFileChange = async (e) => {
const file = e.target.files[0];
if (!file) return;
// 验证文件
if (!validateFile(file)) return;
// 生成预览
const objectUrl = URL.createObjectURL(file);
previewUrl.value = objectUrl;
// 执行上传
try {
uploading.value = true;
const imageUrl = await uploadFile(file);
// 更新模型值
if (props.maxCount === 1) {
emit('update:modelValue', imageUrl);
} else {
const currentValue = Array.isArray(props.modelValue)
? [...props.modelValue]
: [];
emit('update:modelValue', [...currentValue, imageUrl]);
}
emit('upload-success', imageUrl);
} catch (error) {
emit('upload-error', error);
previewUrl.value = '';
} finally {
uploading.value = false;
e.target.value = ''; // 重置input
}
};
// 文件验证
const validateFile = (file) => {
// 文件类型验证
const validTypes = props.accept.split(',').map(t => t.trim());
const isValidType = validTypes.some(type => {
if (type === 'image/*') return file.type.startsWith('image/');
return file.type === type || file.name.endsWith(type.replace('image/', '.'));
});
if (!isValidType) {
console.error(`不支持的文件类型: ${file.type}`);
return false;
}
// 文件大小验证 (MB → bytes)
const maxSizeBytes = props.maxSize * 1024 * 1024;
if (file.size > maxSizeBytes) {
console.error(`文件大小超过限制: ${props.maxSize}MB`);
return false;
}
return true;
};
// 文件上传函数
const uploadFile = (file) => {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('file', file);
const xhr = new XMLHttpRequest();
// 进度处理
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
progressPercent.value = Math.round((e.loaded / e.total) * 100);
}
});
// 完成处理
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const response = JSON.parse(xhr.responseText);
resolve(response.data.url);
} catch {
reject(new Error('Invalid response format'));
}
} else {
reject(new Error(`Upload failed with status ${xhr.status}`));
}
});
// 错误处理
xhr.addEventListener('error', () => {
reject(new Error('Network error'));
});
xhr.open('POST', props.action, true);
xhr.send(formData);
});
};
// 重新上传
const reupload = () => {
triggerFileInput();
};
// 删除图片
const removeImage = () => {
previewUrl.value = '';
emit('update:modelValue', props.maxCount > 1 ? [] : '');
};
</script>
<style scoped>
.image-uploader {
border: 1px dashed #d9d9d9;
border-radius: 8px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: border-color 0.3s;
}
.image-uploader:hover {
border-color: #1890ff;
}
.upload-area, .preview-area {
position: relative;
}
.placeholder {
color: #999;
}
.preview-area img {
max-width: 100%;
max-height: 200px;
display: block;
margin: 0 auto;
}
.actions {
margin-top: 8px;
display: flex;
justify-content: center;
gap: 16px;
}
.progress {
margin-top: 12px;
}
</style>
```
#### Schema 集成示例
```javascript
// 在表单schema中使用
import SchemaImageUpload from '@/components/SchemaImageUpload.vue';
const schema = {
fields: [
{
field: 'avatar',
label: '用户头像',
component: SchemaImageUpload,
props: {
action: '/api/upload/avatar',
maxCount: 1,
maxSize: 2,
crop: true,
aspectRatio: 1
}
},
{
field: 'gallery',
label: '产品图集',
component: SchemaImageUpload,
props: {
action: '/api/upload/gallery',
maxCount: 5,
maxSize: 5
}
}
]
};
```
#### 核心功能说明
1. **响应式设计**:
- 使用 `defineProps` 接收schema配置
- 通过 `v-model` 实现双向绑定
- 支持单图/多图模式切换
2. **验证机制**:
```javascript
// 文件类型验证
const validTypes = props.accept.split(',').map(t => t.trim());
// 文件大小验证 (MB → bytes)
const maxSizeBytes = props.maxSize * 1024 * 1024;
```
3. **上传过程**:
- 使用XMLHttpRequest实现带进度显示的上传
- 支持中断上传(可扩展)
- 自动处理服务器响应
4. **UI交互**:
- 拖拽区域视觉反馈
- 图片预览功能
- 操作按钮(删除/重新上传)
5. **扩展能力**:
```vue
<!-- 自定义上传区域 -->
<SchemaImageUpload>
<template #upload-area>
<div class="custom-upload">
<CameraOutlined />
<span>自定义上传区域</span>
</div>
</template>
</SchemaImageUpload>
```
#### 最佳实践建议
1. **压缩优化**:
```javascript
// 在handleFileChange中添加
const compressedFile = await compressImage(file, {
quality: 0.8,
maxWidth: 1920
});
```
2. **安全增强**:
```javascript
// 添加CSRF token
xhr.setRequestHeader('X-CSRF-TOKEN', csrfToken);
```
3. **错误处理**:
```javascript
// 在uploadFile中添加超时处理
xhr.timeout = 30000;
xhr.ontimeout = () => reject(new Error('请求超时'));
```
阅读全文
相关推荐


















