当前位置: 首页 > news >正文

uni-file-picker vue3二次封装(本地上传 + v-model)

目录

组件核心结构

核心依赖说明

组件参数详解

1. Props 配置

2. Emits 事件

核心方法解析

1. onFileChange - 文件选择处理

2. 动态压缩质量算法

3. 文件列表管理

uni-file-picker 关键参数

样式设计要点

组件功能流程

总结


本文详细解析一个基于 uni-app 框架的图片上传组件实现,该组件集成了文件选择、格式验证、大小限制、图片压缩和上传功能。

组件核心结构

<template><view class="image-uploader"><uni-file-pickermultiple:limit="1"@select="onFileChange":file-extname="fileType"@delete="removeImage":modelValue="uploadedFiles"ref="FilePicker":auto-upload="false"mode="grid"><slot><view class="upload-btn"><uni-icons type="camera-filled" color="#007aff" size="40"></uni-icons></view></slot></uni-file-picker><cmpay-compress ref="Compress"></cmpay-compress></view>
</template><script setup>
let baseUrl = import.meta.env.VITE_BASE_URL
import { computed, ref } from 'vue'
import cmpayCompress from '../cmpay-compress/cmpay-compress.vue'const props = defineProps({fileType: {type: Array,default: () => ['jpg', 'jpeg', 'png', 'gif']},// 最大maxSize: {type: Number,default: 10 // 10},// 最小minSize: {type: Number,default: 100 // 100kb},value: {type: [String, Object, Array],default: ''},//自己的上传地址action: {type: String,default: '/openness-api/h5/checkH5Photo'},// 上传文件字段名name: {type: String,default: 'filePath'},formData: {type: Object,default: () => {return {}}},headers: {type: Object,default: () => {return {}}}
})
const emit = defineEmits(['update:uploaded', 'update:value'])const FilePicker = ref()
const Compress = ref()const uploadedFiles = computed({get: () => {let val = props.valueif (val) {let temp = 1// 首先将值转为数组const list = Array.isArray(val) ? val : val.split(',')// 然后将数组转为对象数组return list.map((item) => {if (typeof item === 'string') {item = {url: item}}// uiditem.pic_md5 = item.pic_md5 || new Date().getTime() + temp++return item})} else {return []}},set: (val) => {let res = listToString(val)emit('update:value', res)}
})
const onFileChange = async (files) => {const file = files.tempFiles[0]// 检查文件格式if (!props.fileType.includes(file.name.split('.').pop().toLowerCase())) {uni.showModal({ title: '提示', content: '不支持的文件格式' })FilePicker.value.clearFiles(0)return}// 检查文件大小const maxBytes = props.maxSize * 1024 * 1024const minBytes = props.minSize * 1024if (file.size > maxBytes) {uni.showModal({title: '提示',content: `文件大小不能超过 ${props.maxSize} MB`})FilePicker.value.clearFiles(0)return}if (file.size < minBytes) {uni.showModal({title: '提示',content: `文件大小不能小于 ${props.minSize} KB`})FilePicker.value.clearFiles(0)return}const fileSize = file.sizeconst quality = getQuality(fileSize)uni.showLoading({ title: '上传中...', mask: true })Compress.value.compress({src: file.path,quality: quality,progress: (res) => {console.log('压缩进度', res)}}).then(async (compressedPath) => {// 压缩成功,开始上传uni.uploadFile({url: baseUrl + props.action,filePath: compressedPath,name: props.name,headers: props.headers,formData: { ...props.formData },success: (uploadFileRes) => {const data = JSON.parse(uploadFileRes.data)emit('update:uploaded', { ...data.data, formData: props.formData }) // 上传结果回调if (data && data.result_code === 'success') {// 上传成功,更新文件列表uploadedFiles.value = [...uploadedFiles.value,{ url: data.data.picUrl, pic_md5: data.data.pic_md5 }]} else {// 上传失败,删除对应文件FilePicker.value.clearFiles(0)uni.showModal({content: data.result_msg || '识别失败,请检查图片是否正确清晰',showCancel: false})}},fail: (error) => {uni.showModal({ title: '提示', content: '上传失败,网络异常' })console.error(error)FilePicker.value.clearFiles(0)},complete: () => {uni.hideLoading()}})}).catch((err) => {uni.hideLoading()uni.showModal({ title: '提示', content: '图片压缩失败: ' + err })FilePicker.value.clearFiles(0)})
}// 计算压缩质量
const getQuality = (fileSize) => {const sizeMB = fileSize / (1024 * 1024)if (sizeMB > 6) return 0.4if (sizeMB > 4) return 0.6return 0.8
}// 对象转成指定字符串分隔
const listToString = (list, separator) => {let strs = ''separator = separator || ','for (const i in list) {strs += list[i].url + separator}return strs != '' ? strs.substring(0, strs.length - 1) : ''
}// 删除图片
const removeImage = (file) => {uploadedFiles.value = uploadedFiles.value.filter((_, index) => index != file.index)
}
</script><style scoped>
.image-uploader {padding: 5px;background-color: #fff;border-radius: 8px;box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}.header {display: flex;justify-content: space-between;margin-bottom: 8px;
}.upload-btn {display: flex;align-items: center;justify-content: center;width: 80px;height: 80px;border: 2px dashed #ddd;border-radius: 8px;color: #bbb;
}
</style>

核心依赖说明

  1. uni-file-picker - 文件选择器组件

    • 官方文档:uni-file-picker 文件选择上传 - DCloud 插件市场

    • 功能:提供文件选择能力,支持多种选择模式

  2. uni-icons - 图标组件

    • 官方文档:uni-icons 图标 - DCloud 插件市场

    • 提供丰富的图标资源

  3. 自定义压缩组件 (cmpay-compress)

<template><view class="compress" v-if="canvasId"><canvas:canvas-id="canvasId":style="{ width: canvasSize.width, height: canvasSize.height }"></canvas></view>
</template><script>
export default {data() {return {pic: '',canvasSize: {width: 0,height: 0},canvasId: ''}},mounted() {// 创建 canvasIdif (!uni || !uni._helang_compress_canvas) {uni._helang_compress_canvas = 1} else {uni._helang_compress_canvas++}this.canvasId = `compress-canvas${uni._helang_compress_canvas}`},methods: {// 压缩compressFun(params) {return new Promise((resolve, reject) => {// 等待图片信息this.getImageInfo(params.src).then((info) => {if (!info) {reject('获取图片信息异常')return}// 设置最大 & 最小 尺寸const maxSize = params.maxSize || 1080const minSize = params.minSize || 640// 当前图片尺寸let { width, height } = info// 非 H5 平台进行最小尺寸校验// #ifndef H5if (width <= minSize && height <= minSize) {resolve(params.src)return}// #endif// 最大尺寸计算//(图像的宽度和高度是否超过最大尺寸。如果其中任一维度超过最大尺寸,代码将对图像进行调整,以使其适应最大尺寸并保持其宽高比。)// 这样可以确保图像在调整大小后仍保持原始比例,并且不会超过指定的最大尺寸if (width > maxSize || height > maxSize) {if (width > height) {height = Math.floor(height / (width / maxSize))width = maxSize} else {width = Math.floor(width / (height / maxSize))height = maxSize}}// 设置画布尺寸this.$set(this, 'canvasSize', {width: `${width}px`,height: `${height}px`})// Vue.nextTick 回调在 App 有异常,则使用 setTimeout 等待DOM更新setTimeout(() => {// 创建 canvas 绘图上下文(指定 canvasId)。在自定义组件下,第二个参数传入组件实例this,以操作组件内 <canvas/> 组件// Tip: 需要指定 canvasId,该绘图上下文只作用于对应的 <canvas/>const ctx = uni.createCanvasContext(this.canvasId, this)// 清除画布上在该矩形区域内的内容。(x,y,宽,高)ctx.clearRect(0, 0, width, height)// 绘制图像到画布。(所要绘制的图片资源,x,y,宽,高)ctx.drawImage(info.path, 0, 0, width, height)// 将之前在绘图上下文中的描述(路径、变形、样式)画到 canvas 中。// 本次绘制是否接着上一次绘制,即reserve参数为false,则在本次调用drawCanvas绘制之前native层应先清空画布再继续绘制;若reserver参数为true,则保留当前画布上的内容,本次调用drawCanvas绘制的内容覆盖在上面,默认 false// 绘制完成后回调ctx.draw(false, () => {// 把当前画布指定区域的内容导出生成指定大小的图片,并返回文件路径。在自定义组件下,第二个参数传入自定义组件实例,以操作组件内 <canvas> 组件。uni.canvasToTempFilePath({x: 0, //画布x轴起点(默认0)y: 0, //画布y轴起点(默认0)width: width, //画布宽度(默认为canvas宽度-x)height: height, //画布高度(默认为canvas高度-ydestWidth: width, //图片宽度(默认为 width * 屏幕像素密度)destHeight: height, //输出图片高度(默认为 height * 屏幕像素密度)canvasId: this.canvasId, //画布标识,传入 <canvas/> 的 canvas-id(支付宝小程序是id、其他平台是canvas-id)fileType: params.fileType || 'png', //目标文件的类型,只支持 'jpg' 或 'png'。默认为 'png'quality: params.quality || 0.9, //图片的质量,取值范围为 (0, 1],不在范围内时当作1.0处理success: (res) => {// 在H5平台下,tempFilePath 为 base64resolve(res.tempFilePath)},fail: (err) => {console.log(err)reject(null)}},this)})}, 300)}).catch((err) => {console.log(err)reject('获取图片信息异常')})})},// 获取图片信息getImageInfo(src) {return new Promise((resolve, reject) => {uni.getImageInfo({src,success: (info) => {resolve(info)},fail: (err) => {console.log(err, 'err===获取图片信息')reject(null)}})})},// 批量压缩async compress(params) {// 初始化状态变量let [index, done, fail] = [0, 0, 0]let paths = []// 处理待压缩图片列表let waitList = Array.isArray(params.src) ? params.src : [params.src]// 批量压缩方法let batch = async () => {while (index < waitList.length) {try {const path = await next()done++paths.push(path)params.progress?.({ done, fail, count: waitList.length })} catch (error) {fail++params.progress?.({ done, fail, count: waitList.length })}index++}}// 单个图片压缩方法let next = () => {const currentSrc = waitList[index]return this.compressFun({src: currentSrc,maxSize: params.maxSize,fileType: params.fileType,quality: params.quality,minSize: params.minSize})}// 返回Promise并处理结果return new Promise((resolve, reject) => {try {batch().then(() => {if (typeof params.src === 'string') {resolve(paths[0])} else {resolve(paths)}}).catch((error) => {reject(error)})} catch (error) {reject(error)}})}}
}
</script><style lang="scss" scoped>
.compress {position: fixed;width: 12px;height: 12px;overflow: hidden;top: -99999px;left: 0;
}
</style>

组件参数详解

1. Props 配置
参数名类型默认值说明
fileTypeArray['jpg','jpeg','png','gif']允许上传的文件类型
maxSizeNumber10 (MB)文件大小上限
minSizeNumber100 (KB)文件大小下限
value[String,Object,Array]''已上传文件数据(支持字符串/对象/数组格式)
actionString/openness-api/h5/checkH5Photo上传接口地址
nameString'filePath'上传文件的字段名
formDataObject{}上传时附加的表单数据
headersObject{}上传请求头配置
2. Emits 事件
事件名说明
update:uploaded上传完成时触发,返回服务器响应数据
update:value文件列表变更时触发,更新绑定值

核心方法解析

1. onFileChange - 文件选择处理
const onFileChange = async (files) => {// 1. 获取文件并验证格式const file = files.tempFiles[0];const ext = file.name.split('.').pop().toLowerCase();if (!props.fileType.includes(ext)) {uni.showModal({ title: '提示', content: '不支持的文件格式' });return;}// 2. 验证文件大小const maxBytes = props.maxSize * 1024 * 1024;const minBytes = props.minSize * 1024;if (file.size > maxBytes) {uni.showModal({ title: '提示', content: `文件大小不能超过 ${props.maxSize} MB` });return;}if (file.size < minBytes) {uni.showModal({ title: '提示', content: `文件大小不能小于 ${props.minSize} KB` });return;}// 3. 计算压缩质量const quality = getQuality(file.size);// 4. 执行压缩uni.showLoading({ title: '上传中...', mask: true });try {const compressedPath = await Compress.value.compress({src: file.path,quality: quality});// 5. 上传文件uni.uploadFile({url: baseUrl + props.action,filePath: compressedPath,name: props.name,headers: props.headers,formData: { ...props.formData },success: (res) => {const data = JSON.parse(res.data);// 处理上传结果...},fail: (error) => {// 错误处理...},complete: () => uni.hideLoading()});} catch (err) {// 压缩错误处理...}
}
2. 动态压缩质量算法
const getQuality = (fileSize) => {const sizeMB = fileSize / (1024 * 1024);if (sizeMB > 6) return 0.4;   // >6MB 使用40%质量if (sizeMB > 4) return 0.6;   // >4MB 使用60%质量return 0.8;                   // 其他使用80%质量
}
3. 文件列表管理
// 计算属性:转换value为可用格式
const uploadedFiles = computed({get: () => {let val = props.value;if (!val) return [];const list = Array.isArray(val) ? val : val.split(',');return list.map((item, index) => ({url: typeof item === 'string' ? item : item.url,pic_md5: item.pic_md5 || Date.now() + index}));},set: (val) => {const urls = val.map(item => item.url);emit('update:value', urls.join(','));}
});// 删除文件处理
const removeImage = (file) => {uploadedFiles.value = uploadedFiles.value.filter((_, index) => index !== file.index);
}

uni-file-picker 关键参数

参数说明
multiple是否支持多选(实际被limit=1限制为单选)
:limit="1"最大选择文件数量
@select文件选择事件
@delete文件删除事件
:file-extname允许的文件扩展名
:modelValue绑定的文件列表
:auto-upload="false"关闭自动上传(手动控制上传流程)
mode="grid"网格显示模式
ref="FilePicker"组件引用,用于调用clearFiles等方法

样式设计要点

.image-uploader {padding: 5px;background-color: #fff;border-radius: 8px;box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); /* 添加微妙阴影 */
}.upload-btn {display: flex;align-items: center;justify-content: center;width: 80px;height: 80px;border: 2px dashed #ddd; /* 虚线边框 */border-radius: 8px;color: #bbb;transition: all 0.3s; /* 添加过渡效果 */
}.upload-btn:active {background-color: #f9f9f9; /* 点击反馈 */
}

组件功能流程

  1. 用户交互:点击相机图标触发文件选择

  2. 文件验证

    • 格式验证(jpg/jpeg/png/gif)

    • 大小验证(100KB-10MB)

  3. 图片处理

    • 根据文件大小动态计算压缩质量

    • 调用压缩组件进行图片压缩

  4. 文件上传

    • 显示加载状态

    • 携带自定义表单数据和请求头

    • 处理上传结果

  5. 状态管理

    • 成功:更新文件列表

    • 失败:显示错误信息

  6. 文件删除

    • 从文件列表中移除项目

    • 更新绑定数据

总结

该图片上传组件提供了完整的文件处理解决方案:

  1. 通过3实现文件选择

  2. 严格的格式和大小验证

  3. 智能的图片压缩策略

  4. 灵活的上传配置(地址/字段/请求头)

  5. 完善的状态管理和错误处理

组件设计考虑了移动端用户体验,包括:

  • 友好的提示信息

  • 加载状态反馈

  • 直观的操作流程

  • 美观的视觉设计

开发者可根据实际需求调整验证规则、压缩算法和UI样式,以适应不同项目需求。

http://www.dtcms.com/a/295811.html

相关文章:

  • Mysql命令show processlist
  • Linux基础服务(autofs和Samba)
  • 论文阅读:《针对多目标优化和应用的 NSGA-II 综述》一些关于优化算法的简介
  • OpenCV —— color_matrix_numpy_mat_reshape
  • 新mac电脑软件安装指南(前端开发用)
  • 解决http的web服务中与https服务交互的问题
  • 平时遇到的错误码及场景?404?400?502?都是什么场景下什么含义,该怎么做 ?
  • AI实践:Pydantic
  • 大模型Prompt优化工程
  • pdf文件的属性值怎么修改?修改PDF内部的属性创建时间和修改时间
  • Lua(垃圾回收)
  • omofun官网网站入口,动漫在线看|官方下载
  • AI服务器给一体成型电感带来多大的市场空间
  • 网络编程——聊天程序实现
  • FreeSWITCH 简单图形化界面45 - 收集打包的一些TTS
  • 复矩阵与共轭转置矩阵乘积及其平方根矩阵
  • 【建模与仿真】融合共现网络特征与知识增强语义梯度提升电子邮件分类
  • HttpServletRequest深度解析:Java Web开发的核心组件
  • LLM中的位置嵌入矩阵(Position Embedding Matrix)是什么
  • [语言模型训练]基于 PyTorch 的双向 LSTM 文本分类器实现:基于旅店的评论分类语言模型
  • LeetCode 2563.统计公平数对的数目
  • Edwards爱德华泵软件 支持nEXT85和nXDS系列泵,包括nXRi, nRVi和nXLi增强型 nEXT nXDS nXLi
  • 【自动化测试】JMeter+Jenkins自动化接口与性能测试环境部署指南
  • Java学习第七十部分——微服务架构
  • JavaWeb_原始项目初识(一)
  • ubuntu25.04+4070+cuda+docker安装
  • 进程通信————命名管道
  • Python-初学openCV——图像预处理(二)
  • 怎么样知道服务端是否支持sse服务?
  • 在 Ubuntu 20.04 上轻松安装和使用中文输入法