网站建设与管理的实训报告wordpress 微信登陆
网站建设与管理的实训报告,wordpress 微信登陆,旅游信息网站建设论文,广告设计专业周记问题背景
在小程序开发中#xff0c;文件上传是常见功能。但当用户上传较大的文件#xff08;如会议决议文档、合同PDF等#xff09;时#xff0c;往往会遇到体验问题#xff1a;
实际场景#xff1a; 用户上传一个 40MB 的决议文件#xff0c;上传耗时超过 1 分钟。 …问题背景在小程序开发中文件上传是常见功能。但当用户上传较大的文件如会议决议文档、合同PDF等时往往会遇到体验问题实际场景用户上传一个 40MB 的决议文件上传耗时超过 1 分钟。问题现象页面没有任何上传进度反馈用户不知道是否在上传上传期间用户可以点击其他按钮、甚至退出页面误操作导致上传中断用户需要重新上传体验像卡住了但还能乱点用户视角 ┌─────────────────────────────────────┐ │ 上传决议文件 │ │ │ │ [选择文件] 会议决议.pdf │ │ │ │ [提交] ← 点了没反应再点一下 │ │ │ │ ← 返回 ← 算了不传了退出吧 │ └─────────────────────────────────────┘ 实际情况文件正在上传中但用户完全不知道问题分析1. 前端缺少上传状态管理// 问题代码constuploadFileasync(){// 直接调用上传没有任何状态管理constresawaitapi.upload(file)// 用户在等待期间可以随意操作页面}2. 前端未暴露上传进度// 问题代码通用上传函数没有进度回调exportconstuploadFile(options){returnnewPromise((resolve,reject){uni.uploadFile({url:options.url,filePath:options.filePath,name:file,success:resolve,fail:reject// 缺少进度监听})})}3. 后端资源初始化重复// 问题代码每次上传都重新初始化 TikapublicStringdetectFileType(InputStreaminput){TikaConfigconfignewTikaConfig();// 每次都 new耗时TikaInputStreamstreamTikaInputStream.get(input);// 没有关闭 stream资源泄漏returndetector.detect(stream,metadata).toString();}解决方案方案一上传进度监听改造通用上传函数暴露进度回调// utils/file.js/** * 文件上传支持进度回调 * param {Object} options * param {string} options.url - 上传地址 * param {string} options.filePath - 文件临时路径 * param {string} options.name - 文件字段名 * param {Object} options.formData - 额外表单数据 * param {Function} options.onBeforeUpload - 上传前回调 * param {Function} options.onProgress - 进度回调 (progress: 0-100) * returns {Promise} */exportconstuploadFile(options){returnnewPromise((resolve,reject){// 上传前回调options.onBeforeUpload?.()// 创建上传任务constuploadTaskuni.uploadFile({url:options.url,filePath:options.filePath,name:options.name||file,formData:options.formData||{},header:{Authorization:Bearer${getToken()}},success:(res){if(res.statusCode200){try{constdataJSON.parse(res.data)resolve(data)}catch(e){resolve(res.data)}}else{reject(newError(上传失败:${res.statusCode}))}},fail:(err){reject(err)}})// 监听上传进度uploadTask.onProgressUpdate((res){// res.progress: 上传进度百分比 (0-100)// res.totalBytesSent: 已上传的数据长度// res.totalBytesExpectedToSend: 预期需要上传的数据总长度options.onProgress?.(res.progress,res)})})}方案二上传状态管理在页面中管理上传状态禁止用户误操作template view classupload-page !-- 上传遮罩层 -- view v-ifisUploading classupload-overlay view classupload-modal view classupload-icon text classiconfont icon-upload/text /view text classupload-text文件上传中.../text view classupload-progress-bar view classupload-progress-inner :style{ width: uploadProgress % } /view /view text classupload-percent{{ uploadProgress }}%/text text classupload-tip请勿离开当前页面/text /view /view !-- 页面内容 -- view classcontent view classfile-section text classlabel决议文件/text view classfile-picker tapchooseFile text v-if!selectedFile点击选择文件/text text v-else{{ selectedFile.name }}/text /view /view button classsubmit-btn :disabledisUploading taphandleSubmit {{ isUploading ? 上传中... : 提交 }} /button /view /view /template script setup import { ref } from vue import { onBackPress } from dcloudio/uni-app import { uploadFile } from /utils/file // 上传状态 const isUploading ref(false) const uploadProgress ref(0) const selectedFile ref(null) // 选择文件 const chooseFile () { if (isUploading.value) return uni.chooseMessageFile({ count: 1, type: file, success: (res) { selectedFile.value res.tempFiles[0] } }) } // 提交上传 const handleSubmit async () { if (!selectedFile.value) { uni.showToast({ title: 请选择文件, icon: none }) return } if (isUploading.value) return try { isUploading.value true uploadProgress.value 0 const result await uploadFile({ url: /api/file/upload, filePath: selectedFile.value.path, name: file, formData: { meetingId: meetingId.value }, onBeforeUpload: () { console.log(开始上传) }, onProgress: (progress) { uploadProgress.value progress } }) uni.showToast({ title: 上传成功, icon: success }) // 处理上传成功后的逻辑 } catch (error) { uni.showToast({ title: 上传失败请重试, icon: none }) } finally { isUploading.value false uploadProgress.value 0 } } // 拦截页面返回 onBackPress(() { if (isUploading.value) { uni.showModal({ title: 提示, content: 文件正在上传中离开将中断上传确定要离开吗, success: (res) { if (res.confirm) { // 用户确认离开可以取消上传任务 isUploading.value false uni.navigateBack() } } }) return true // 阻止默认返回行为 } return false // 允许返回 }) /script style scoped .upload-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; z-index: 999; } .upload-modal { width: 500rpx; padding: 60rpx 40rpx; background: #fff; border-radius: 24rpx; display: flex; flex-direction: column; align-items: center; } .upload-icon { width: 120rpx; height: 120rpx; background: #e8f5e9; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 32rpx; } .upload-icon .iconfont { font-size: 60rpx; color: #4caf50; } .upload-text { font-size: 32rpx; color: #333; font-weight: 500; margin-bottom: 32rpx; } .upload-progress-bar { width: 100%; height: 16rpx; background: #e0e0e0; border-radius: 8rpx; overflow: hidden; margin-bottom: 16rpx; } .upload-progress-inner { height: 100%; background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 8rpx; transition: width 0.3s ease; } .upload-percent { font-size: 28rpx; color: #4caf50; font-weight: 500; margin-bottom: 24rpx; } .upload-tip { font-size: 24rpx; color: #999; } .submit-btn { margin-top: 40rpx; background: #4caf50; color: #fff; border-radius: 44rpx; } .submit-btn[disabled] { background: #ccc; } /style方案三封装上传组件将上传逻辑封装为可复用的组合式函数// composables/useUpload.jsimport{ref}fromvueimport{onBackPress}fromdcloudio/uni-appimport{uploadFile}from/utils/file/** * 文件上传组合式函数 * param {Object} options * param {string} options.url - 上传地址 * param {boolean} options.blockBack - 是否阻止返回默认 true */exportconstuseUpload(options{}){const{url,blockBacktrue}optionsconstisUploadingref(false)constprogressref(0)consterrorref(null)// 执行上传constuploadasync(file,formData{}){if(isUploading.value){console.warn(上传进行中请勿重复调用)returnnull}isUploading.valuetrueprogress.value0error.valuenulltry{constresultawaituploadFile({url,filePath:file.path||file,name:file,formData,onProgress:(p){progress.valuep}})returnresult}catch(err){error.valueerrthrowerr}finally{isUploading.valuefalse}}// 拦截返回if(blockBack){onBackPress((){if(isUploading.value){uni.showToast({title:文件上传中请稍候,icon:none})returntrue}returnfalse})}return{isUploading,progress,error,upload}}使用方式script setup import { useUpload } from /composables/useUpload const { isUploading, progress, upload } useUpload({ url: /api/upload }) const handleUpload async (file) { try { const result await upload(file, { type: resolution }) console.log(上传成功, result) } catch (err) { console.error(上传失败, err) } } /script template upload-overlay v-ifisUploading :progressprogress / /template方案四后端性能优化优化文件类型检测避免重复初始化// SecurityFileService.javaServicepublicclassSecurityFileService{// 静态复用 TikaConfig避免重复初始化privatestaticfinalTikaConfigTIKA_CONFIG;privatestaticfinalDetectorDETECTOR;static{try{TIKA_CONFIGnewTikaConfig();DETECTORTIKA_CONFIG.getDetector();}catch(Exceptione){thrownewRuntimeException(Failed to initialize Tika,e);}}/** * 检测文件类型 * param input 文件输入流 * param fileName 文件名 * return MIME 类型 */publicStringdetectFileType(InputStreaminput,StringfileName){// 使用 try-with-resources 确保流关闭try(TikaInputStreamtikaStreamTikaInputStream.get(input)){MetadatametadatanewMetadata();metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY,fileName);MediaTypemediaTypeDETECTOR.detect(tikaStream,metadata);returnmediaType.toString();}catch(Exceptione){log.warn(Failed to detect file type, fallback to octet-stream,e);returnapplication/octet-stream;}}}完整的上传遮罩组件封装一个通用的上传遮罩组件!-- components/upload-overlay.vue -- template view v-ifvisible classupload-overlay tap.stop view classupload-modal !-- 上传动画 -- view classupload-animation view classupload-arrow/view view classupload-cloud/view /view !-- 进度信息 -- text classupload-title{{ title }}/text view classprogress-container view classprogress-bar view classprogress-inner :style{ width: progress % } /view /view text classprogress-text{{ progress }}%/text /view !-- 文件信息 -- view v-iffileName classfile-info text classfile-name{{ fileName }}/text text classfile-size{{ formatSize(fileSize) }}/text /view !-- 提示 -- text classupload-tip{{ tip }}/text !-- 取消按钮可选 -- button v-ifcancelable classcancel-btn tap$emit(cancel) 取消上传 /button /view /view /template script setup defineProps({ visible: { type: Boolean, default: false }, progress: { type: Number, default: 0 }, title: { type: String, default: 文件上传中... }, tip: { type: String, default: 请勿离开当前页面 }, fileName: { type: String, default: }, fileSize: { type: Number, default: 0 }, cancelable: { type: Boolean, default: false } }) defineEmits([cancel]) // 格式化文件大小 const formatSize (bytes) { if (!bytes) return if (bytes 1024) return bytes B if (bytes 1024 * 1024) return (bytes / 1024).toFixed(1) KB return (bytes / 1024 / 1024).toFixed(1) MB } /script style scoped .upload-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; z-index: 9999; } .upload-modal { width: 560rpx; padding: 60rpx 48rpx; background: #fff; border-radius: 32rpx; display: flex; flex-direction: column; align-items: center; } .upload-animation { width: 160rpx; height: 160rpx; position: relative; margin-bottom: 40rpx; } .upload-cloud { width: 100%; height: 100%; background: #e3f2fd; border-radius: 50%; } .upload-arrow { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 0; height: 0; border-left: 24rpx solid transparent; border-right: 24rpx solid transparent; border-bottom: 40rpx solid #2196f3; animation: upload-bounce 1s ease-in-out infinite; } keyframes upload-bounce { 0%, 100% { transform: translate(-50%, -50%); } 50% { transform: translate(-50%, -70%); } } .upload-title { font-size: 36rpx; font-weight: 600; color: #333; margin-bottom: 32rpx; } .progress-container { width: 100%; display: flex; align-items: center; gap: 20rpx; margin-bottom: 24rpx; } .progress-bar { flex: 1; height: 20rpx; background: #e0e0e0; border-radius: 10rpx; overflow: hidden; } .progress-inner { height: 100%; background: linear-gradient(90deg, #2196f3, #03a9f4); border-radius: 10rpx; transition: width 0.3s ease; } .progress-text { font-size: 28rpx; font-weight: 600; color: #2196f3; min-width: 80rpx; text-align: right; } .file-info { display: flex; align-items: center; gap: 16rpx; margin-bottom: 24rpx; } .file-name { font-size: 26rpx; color: #666; max-width: 300rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .file-size { font-size: 24rpx; color: #999; } .upload-tip { font-size: 26rpx; color: #999; margin-top: 16rpx; } .cancel-btn { margin-top: 32rpx; padding: 16rpx 48rpx; background: #f5f5f5; color: #666; font-size: 28rpx; border-radius: 32rpx; } /style最佳实践总结1. 上传状态三要素要素说明实现方式进度反馈让用户知道上传在进行中onProgressUpdate回调禁止操作防止用户误操作中断上传遮罩层 按钮禁用返回拦截防止用户退出页面onBackPress钩子2. 上传函数设计原则// ✅ 好的设计暴露完整的生命周期钩子uploadFile({url,filePath,onBeforeUpload,// 上传前onProgress,// 进度更新onSuccess,// 成功回调onError,// 失败回调onComplete// 完成回调无论成功失败})3. 用户体验优化清单显示上传进度百分比显示文件名和大小遮罩层阻止误操作拦截页面返回提供取消上传选项可选上传失败给出明确提示支持断点续传高级4. 后端配合优化资源复用如 TikaConfig使用 try-with-resources 管理流合理设置超时时间支持分片上传大文件总结用户体验是核心文件上传耗时不可控必须给用户明确的反馈和保护进度回调是关键uni.uploadFile返回的uploadTask支持进度监听一定要用起来防误操作是必须遮罩层 返回拦截双重保障组件化便于复用封装通用的上传遮罩组件和useUpload组合式函数前后端协作优化前端体验 后端性能缺一不可本文源于实际项目中的性能优化实践解决了文件上传体验差的问题。希望能帮助其他开发者构建更好的文件上传体验。