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

vue3 + ts + uniappX 封装上传文件(image pdf)、预览文件功能

注: 使用uniapp的,将文件后缀改为vue就行

<!-- uploadFiles.uvue -->
<template><view class="upload-container"><view class="u-flex-x"><!-- 文件上传区域 --><view v-if="uploadType === 'file' || uploadType === 'both' && fileList.length < maxCount" class="upload-section"><!-- 如果文件类型还有doc等,可设置accept=".pdf, .doc, .docx" --><up-upload @afterRead="onFileAfterRead" @delete="onFileDelete" :maxCount="maxCount"multiple accept=".pdf" :disabled="uploading"style="margin-right:16px;" ><view class="upload-btn" v-if="fileList.length < maxCount"><u-icon name="plus" size="20" color="#606266"></u-icon><text class="upload-text">添加文件</text><text class="upload-tips">(仅支持pdf文件)</text></view></up-upload></view><!-- 图片上传区域 --><view v-if="uploadType === 'image' || uploadType === 'both' && imageList.length < maxCount" class="upload-section"><up-upload @afterRead="onImageAfterRead" @delete="onImageDelete" :maxCount="maxCount" multiple :disabled="uploading"><view class="upload-btn" v-if="imageList.length < maxCount"><u-icon name="plus" size="20" color="#606266"></u-icon><text class="upload-text">添加图片</text></view></up-upload></view><!-- 上传进度 如果需要可解禁--><!-- <view v-if="uploading" class="progress-container"><progress class="progress-bar" :percent="progressPercent" show-info stroke-width="4" /><text class="progress-text">上传中... {{ progressPercent }}%</text></view> --></view><!-- 多文件展示 --><view class="" v-if="fileList.length > 0 || imageList.length > 0"><!-- props.imgStyle=='round' ? 圆矩形样式展示(仅上传一个图片时需要此展示效果) : 列表样式展示  (多文件用列表展示)  --><view class="" v-if="props.imgStyle=='round'"><view class="show-edit-img"><!-- 设置图片大小,默认60*60px,个别地方可能设为20*20px --><image :src="imageList[0].url" mode="" :class="props.imgSize == 'mini' ? 'mini': 'default'"></image><view class="camera-close" @click="imageList=[]"><text class="close-icon">×</text></view></view></view><view class="" v-else><!-- 展示文件列表 --><view class="file-item" v-for="(file, index) in fileList" :key="index"><up-loading-icon v-if="uploading && uploadType === 'file' && index == fileList.length-1" mode="semicircle" size="14"></up-loading-icon><view class="" v-else><image v-if="getWebViewFileType(file.url) == 'pdf'" src="/static/icons/learn-base/pdf.png" mode=""></image><image v-else src="/static/icons/learn-base/file_word.png" mode=""></image></view><text class="ellipsis" @click="previewFile(file.url)">{{file.name}}</text><up-icon name="trash" size="16" color="#fa3534" @click="onFileDelete(index)"></up-icon></view><!-- 展示图片列表 --><view class="file-item" v-for="(file, index) in imageList" :key="index"><up-loading-icon v-if="uploading && uploadType === 'image' && index == imageList.length-1" mode="semicircle" size="14"></up-loading-icon><view class="image-item" v-else  @click="previewImage(index)"><image :src="file.url" mode=""></image></view><text class="ellipsis" @click="previewImage(index)">{{file.name}}</text><up-icon name="trash" size="16" color="#fa3534" @click="onImageDelete(index)"></up-icon></view></view></view></view>
</template><script setup lang="ts">import { preWebViewFile, // 预览文件getWebViewFileType, // 获取文件类型(pdf、docx、doc)} from '@/utils/filePreview';import { ref, computed, watch, onMounted } from 'vue'// 定义OSS配置const OSS_CONFIG = {baseURL: 'https://labmanagement.oss-xxx.aliyuncs.com', // oss地址 拼接上传后返回的地址,用于回显 imageUploadUrl: 'https://sci.gewu.pro/api/wxNewAction/uploadImg', // 上传图片fileUploadUrl: 'https://sci.gewu.pro/api/wxNewAction/uploadFile', // 上传文件}// 定义组件属性interface Props {modelValue ?: string[] // 已上传的文件URL数组uploadType ?: 'file' | 'image' | 'both' // 上传类型 file-文件,image-图片,both-都可(展示两上传框)maxCount ?: number // 最大上传数量autoUpload ?: boolean // 是否选择后自动上传uniqueKey: string // 标识符imgStyle: string // 图片样式 圆角框 / 列表imgSize: string // 图片大小}const props = withDefaults(defineProps<Props>(), {modelValue: () => [],uploadType: 'both',maxCount: 1,autoUpload: true,imgStyle: 'round' // round | list});// 定义事件const emit = defineEmits<{(e : 'update:modelValue', value : string[]) : void(e : 'upload-success', value : string[]) : void(e : 'upload-fail', error : any) : void}>()// 响应式数据const fileList = ref<Array<{ url : string, name : string, size : number, key: string }>>([])const imageList = ref<Array<{ url : string, name : string, size : number, key: string }>>([])const uploading = ref(false)const progressPercent = ref(0)const uploadTasks = ref<Set<number>>(new Set())const instanceId = ref(Date.now() + Math.random().toString(36).substr(2, 5))// 计算已上传的URLconst uploadedUrls = computed(() => {const files = {urls: [],key: ''}fileList.value.forEach(file => {if (file.url.startsWith(OSS_CONFIG.baseURL)) {files.urls.push(file.url);files.key = file.key;}})imageList.value.forEach(image => {if (image.url.startsWith(OSS_CONFIG.baseURL)) {files.urls.push(image.url);files.key = image.key;}});return files})// 监听已上传URL变化watch(uploadedUrls, (newUrls) => {emit('update:modelValue', newUrls)emit('upload-success', newUrls)}, { deep: true })// 初始化回显数据onMounted(() => {if (props.modelValue && props.modelValue.length) {const initFile = ref([]);if (props.modelValue.includes(',')) {initFile.value = props.modelValue.split(',');} else {initFile.value = [props.modelValue];}initFile.value.forEach(url => {if (isImageFile(url)) {imageList.value.push({url,name: url.split('/').pop() || 'image',size: 0,key: props.uniqueKey})} else {fileList.value.push({url,name: url.split('/').pop() || 'file',size: 0,key: props.uniqueKey})}})}})// 判断是否为图片文件const isImageFile = (url : string) : boolean => {const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']return imageExtensions.some(ext => url.toLowerCase().includes(ext))}// 文件读取后处理const onFileAfterRead = async (event : any) => {const files = event.file.map((file : any) => ({url: file.url,name: file.name,size: file.size,file: file}))// 检查文件类型for (const file of files) {const ext = file.name.split('.').pop()?.toLowerCase()if (!['pdf'].includes(ext || '')) {uni.showToast({title: `文件 ${file.name} 格式不支持`,icon: 'none'})return}}// 添加到文件列表fileList.value = [...fileList.value, ...files];emit('upload-success', fileList.value)// 自动上传if (props.autoUpload) {for (const file of files) {await uploadFile(file)}}}// 图片读取后处理const onImageAfterRead = async (event : any) => {const images = event.file.map((image : any) => ({url: image.url,name: image.name,size: image.size,file: image}))// 添加到图片列表imageList.value = [...imageList.value, ...images];// 自动上传if (props.autoUpload) {for (const image of images) {await uploadImage(image)}}}// 删除文件const onFileDelete = (index : number) => {fileList.value.splice(index, 1)}// 删除图片const onImageDelete = (index : number) => {imageList.value.splice(index, 1)}// 上传文件const uploadFile = async (fileInfo : any) : Promise<string> => {uploading.value = trueconst taskId = Date.now()uploadTasks.value.add(taskId)try {return await simpleUpload(fileInfo, false);} catch (error) {console.error('上传失败:', error)uni.showToast({title: `文件 ${fileInfo.name} 上传失败`,icon: 'none'})emit('upload-fail', error)throw error} finally {uploadTasks.value.delete(taskId)if (uploadTasks.value.size === 0) {uploading.value = falseprogressPercent.value = 0}}}// 上传图片const uploadImage = async (imageInfo : any) : Promise<string> => {uploading.value = trueconst taskId = Date.now()uploadTasks.value.add(taskId)try {return await simpleUpload(imageInfo, true);} catch (error) {console.error('上传失败:', error)uni.showToast({title: `图片 ${imageInfo.name} 上传失败`,icon: 'none'})emit('upload-fail', error)throw error} finally {uploadTasks.value.delete(taskId)if (uploadTasks.value.size === 0) {uploading.value = false}}}// 上传const simpleUpload = async (fileInfo : any, isImage : boolean) : Promise<string> => {return new Promise((resolve, reject) => {const uploadUrl = isImage ? OSS_CONFIG.imageUploadUrl : OSS_CONFIG.fileUploadUrl;const uploadTask = uni.uploadFile({url: uploadUrl,filePath: fileInfo.url,name: 'file',formData: {name: fileInfo.name,instanceId: instanceId.value,isChunk: 'false'},header: { openid: uni.getStorageSync("openid") }, // 我们的需求是上传文件时必须加openid, 根据自己需要改动success: (res) => {if (res.statusCode === 200) {try {const data = JSON.parse(res.data)if (data.code === 0) {const ossUrl = OSS_CONFIG.baseURL + data.data.url;// 更新URLif (isImage) {const index = imageList.value.findIndex(img => img.url === fileInfo.url)if (index !== -1) {imageList.value[index].url = ossUrl;imageList.value[index].key = props.uniqueKey;}} else {const index = fileList.value.findIndex(f => f.url === fileInfo.url)if (index !== -1) {fileList.value[index].url = ossUrl;fileList.value[index].key = props.uniqueKey;}}resolve(ossUrl)} else {reject(new Error(data.msg || '上传失败'))}} catch (e) {reject(new Error('解析响应失败'))}} else {reject(new Error(`上传失败,状态码: ${res.statusCode}`))}},fail: (error) => {reject(new Error(error.errMsg || '上传失败'))}})// 监听上传进度uploadTask.onProgressUpdate((progress) => {progressPercent.value = progress.progress})})}// 手动触发上传const uploadAll = async () : Promise<void> => {const uploadPromises : Promise<string>[] = []// 上传所有未上传的文件fileList.value.forEach(file => {if (!file.url.startsWith(OSS_CONFIG.baseURL)) {uploadPromises.push(uploadFile(file))}})// 上传所有未上传的图片imageList.value.forEach(image => {if (!image.url.startsWith(OSS_CONFIG.baseURL)) {uploadPromises.push(uploadImage(image))}})try {await Promise.all(uploadPromises)uni.showToast({title: '上传成功',icon: 'success'})} catch (error) {// 错误已在单独上传方法中处理}}// 预览图片const previewImage = (index: number): void => {const images = imageList.value.filter(img => img.url.startsWith(OSS_CONFIG.baseURL)).map(img => img.url)const current = images.indexOf(imageList.value[index].url)if (current !== -1) {uni.previewImage({urls: images,current: images[current]})}}// 预览文件const previewFile = (fileUrl : string) : void => {const fileType = getWebViewFileType(fileUrl);preWebViewFile(fileUrl, fileType);}// 暴露方法给父组件defineExpose({uploadAll,clear: () => {fileList.value = []imageList.value = []}})
</script><style scoped lang="scss">/* 样式保持不变 */.upload-container {padding: 10rpx;}.upload-section {margin-bottom: 20rpx;}.upload-btn {display: flex;align-items: center;justify-content: center;flex-direction: column;width: 160rpx;height: 160rpx;background-color: #f7f7f7;border-radius: 10rpx;border: 1px dashed #dcdfe6;}.upload-text {font-size: 24rpx;color: #606266;margin-top: 10rpx;}.upload-tips {font-size: 20rpx;color: #909399;margin-top: 5rpx;}.progress-container {padding: 20rpx 0;}.progress-bar {width: 100%;}.progress-text {display: block;text-align: center;font-size: 24rpx;color: #606266;margin-top: 10rpx;}.file-item {background: #dce7fb;margin-bottom: 5px;padding: 6px 5px;border-radius: 3px;display: flex;flex-direction: row;align-items: center;justify-content: space-between;text {max-width: 160px;padding: 0 3px;}image{width: 20px;height: 20px;}.image-item{width:22px;height: 22px;background-color: #fff;display: flex;align-items: center;justify-content: center;margin-right: 3px;border-radius: 3px;}}.show-edit-img{position: relative;overflow: hidden !important;display: flex;flex-direction: row;.default{width: 60px;height: 60px;}.mini{width: 30px;height: 30px;}.camera-close {width: 20px;height: 20px;border-radius: 8px;position: absolute;top: -5px;right: -5px;background-color: rgba(0, 0, 0, 0.8);.close-icon {font-size: 13px;color: #fff;position: absolute;bottom: 2px;left: 4px;}}}</style>

预览文件

// @/utils/filePreview.uts
// 定义文件类型
export type FileType = 'pdf' | 'doc' | 'docx';// 文件预览函数
export const preWebViewFile = async (filePath: string, fileType: FileType) => {if (fileType === 'pdf') {uni.downloadFile({url: filePath,success: (res) => {if (res.statusCode === 200) {openFile(res.tempFilePath);}}});} else {const onlinePreviewUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(filePath)}`;uni.navigateTo({url: `/pages/filePreview/webview?url=${encodeURIComponent(onlinePreviewUrl)}`});}
};// 获取文件类型
export const getWebViewFileType = (fileName: string): FileType | null => {const extension = fileName.split('.').pop()?.toLowerCase();if (extension === 'pdf') return 'pdf';if (extension === 'doc') return 'doc';if (extension === 'docx') return 'docx';return null;
};// 新增:文件下载函数(修复保存问题)
export function downloadFile(fileUrl, fileName = '') {uni.showLoading({ title: '准备下载...' });// 如果没有指定文件名,从URL中提取if (!fileName) {fileName = fileUrl.split('/').pop().split('?')[0];}// 微信下载文件uni.downloadFile({url: fileUrl,success: (res) => {uni.hideLoading();if (res.statusCode === 200) {saveFileToLocal(res.tempFilePath, fileName);} else {uni.showToast({ title: '下载失败', icon: 'none' });}},fail: (err) => {uni.hideLoading();uni.showToast({ title: '下载失败', icon: 'none' });console.error('下载失败:', err);}});
}/*** 保存文件到本地* @param {String} tempFilePath 临时文件路径* @param {String} fileName 文件名*/
function saveFileToLocal(tempFilePath, fileName) {if (uni.getSystemInfoSync().platform === 'ios') { // iOS系统使用saveFileToAlbum方案saveFileToAlbum(tempFilePath, fileName); } else { // android系统uni.getFileSystemManager().saveFile({tempFilePath: tempFilePath,success: (res) => {requestAndroidPermission(res.savedFilePath);},fail: (err) => {uni.showToast({ title: '保存失败', icon: 'none' });console.error('保存失败:', err);}});}
}// 安卓保存
function requestAndroidPermission(filePath) {uni.getSetting({success: (res) => {if (!res.authSetting['scope.writePhotosAlbum']) {uni.authorize({scope: 'scope.writePhotosAlbum',success: () => {openFile(filePath);},fail: () => {uni.showModal({title: '权限申请',content: '需要存储权限才能保存文件,是否去设置开启?',success: (res) => {if (res.confirm) {uni.openSetting();}}});}});} else {openFile(filePath);}}});
}
// 苹果保存
function saveFileToAlbum(tempFilePath, fileName) {// 1. 先保存到系统相册(适用于图片/视频)if (['jpg', 'jpeg', 'png', 'gif', 'mp4', 'mov'].includes(getFileType(tempFilePath))) {uni.saveFileToPhotosAlbum({filePath: tempFilePath,success: () => {uni.showToast({ title: '已保存到相册', icon: 'success' });},fail: () => {// 2. 如果相册保存失败,使用文件分享功能shareFile(tempFilePath, fileName);}});} // 2. 其他文件类型直接使用分享功能else {shareFile(tempFilePath, fileName);}
}function openFile(filePath) {uni.openDocument({filePath: filePath,showMenu: true,success: (res) => {console.log('打开文档成功');},fail: (err) => {console.error('打开文档失败:', err);uni.showToast({ title: '打开文件失败', icon: 'none' });}});
}

父组件

<template><view><!--modelValue: 图片值unique-key:唯一标识符(同一页面可能存在多处上传文件的需求)upload-type:要上传的文件类型 image为图片 file为文件 both两者都有  (默认both)max-count:最多上传文件个数img-style:返回文件的展示样式 round圆矩形 list列表 (默认为round)@upload-success: 成功回调 返回格式:{key: '唯一标识符', urls: ['https://xxx.png', 'hppts://xxx.pdf']}--><!-- 上传图片 --><UploadFiles:modelValue="form.cover_img" unique-key="cover_img"upload-type="image"@upload-success="handleUploadComplete"/><!-- 上传pdf文件 --><UploadFiles:modelValue="form.content_link" unique-key="content_link"upload-type="file":max-count="5"img-style="list"@upload-success="handleUploadComplete"/></view>
</template><script setup>import UploadFiles from "@/components/upload/uploadFiles.uvue";const handleUploadComplete = (file) => {if (file && file.urls.length) {if (file.key == 'cover_img') {// 值类型为字符串form.value.cover_img = file.urls.join(',');// 值类型为数组// form.value.cover_img = file.urls;} else {form.value.content_link = file.urls.join(',');}}}
</script>

文章转载自:

http://IyRoo3eb.msbct.cn
http://dyjKrLEW.msbct.cn
http://MH7v9ul7.msbct.cn
http://m9QkuEx7.msbct.cn
http://5JC6Jlpt.msbct.cn
http://oZPjwqTI.msbct.cn
http://zv7d33vB.msbct.cn
http://MmgJVTBP.msbct.cn
http://MRJ0OPJu.msbct.cn
http://pc2d4QSg.msbct.cn
http://2l72peU6.msbct.cn
http://5HcItL16.msbct.cn
http://yhWqNGzL.msbct.cn
http://v6EJJS8r.msbct.cn
http://RcShaPcz.msbct.cn
http://LKvpFU2g.msbct.cn
http://mr1T34ff.msbct.cn
http://0H1NCnO7.msbct.cn
http://eoLd112Q.msbct.cn
http://H5WE5jS8.msbct.cn
http://lVXo9DJ9.msbct.cn
http://ilpX4jk4.msbct.cn
http://C7OHmiXE.msbct.cn
http://ZIjUDzTC.msbct.cn
http://PnvfJW8A.msbct.cn
http://r6i9K92M.msbct.cn
http://gSGXtA5D.msbct.cn
http://jcEGn0UP.msbct.cn
http://jBAbIgGr.msbct.cn
http://30jwh56p.msbct.cn
http://www.dtcms.com/a/386030.html

相关文章:

  • PDF/图像/音视频一体化处理方案
  • 【数据结构】 深入理解 LinkedList 与链表
  • Hadoop HDFS-高可用集群部署
  • 深入汇编底层与操作系统系统调用接口:彻底掰开揉碎c语言简单的一行代码-打印helloworld是如何从C语言点击运行到显示在屏幕上的
  • ARM3.(汇编函数和c语言相互调用及ARM裸机开发环境搭建)
  • LeetCode 380 - O(1) 时间插入、删除和获取随机元素
  • 9 基于机器学习进行遥感影像参数反演-以随机森林为例
  • DB Hitek宣布推出650V GaN HEMT工艺
  • 机器学习简单数据分析案例
  • [特殊字符] 欢迎使用 C++ Arrow 函数 - 革命性的新特性!
  • 外网访问分布式跟踪系统 zipkin
  • Base 发币在即:L2 代币能否撬动生态增长?
  • DRDR生态Token正式上线BitMart,开启全球化新篇章
  • Spring Boot 3 + EasyExcel 文件导入导出实现
  • 9.16总结
  • Android开机时间查看
  • 探针水平的表达矩阵转换为基因水平的表达矩阵是芯片数据分析中关键的一步
  • PHP基础-语法初步(第七天)
  • 奥威BI与ChatBI:自然语言交互赋能企业数据分析新体验
  • Vue: 组件基础
  • 亚马逊云科技 EC2 服务终端节点:安全高效访问云服务的利器
  • 2026届计算机毕业设计选题 大数据毕业设计选题推荐 题目新颖 数据分析 可视化大屏 通过率高
  • html实现文字横向对齐以及margin的解释
  • 如何轻松找到并畅玩Edge浏览器隐藏的冲浪小游戏
  • K8S中的神秘任务Job与CronJob
  • go grpc开发使用
  • [论文阅读] 人工智能 + 软件工程 | 告别冗余HTML与高算力消耗:EfficientUICoder如何破解UI2Code的token难题
  • Golang语言入门篇004_Go命令详解
  • K8S的Pod状态处理指南
  • Gin框架:构建高性能Go Web应用