vue3 图片上传,文件上传,视频上传。图片预览,视频预览。 电脑版发表于:2024/12/9 18:31 tn2>直接调用接口上传参考:https://www.tnblog.net/notebook/article/details/8495 [TOC] ### 基础的图片上传,文件上传,视频上传 #### 界面就是这样的: <img src="https://img.tnblog.net/arcimg/aojiancc2/a1711cca78b6486e87655a44f8e08925.png" style="width:266px;height:auto;"> #### 界面当中的部分代码如下 ``` <div class="img-waper"> <div class="add-btn" @click="addImgClick"> <input ref="inputImgFile" @change="uploadimg" @click.stop="inputImgFileClick" type="file" multiple="true" style="display: none" accept="image/*,video/*" /> <div class="icon-waper"> <div class="icon-x" /> <div class="icon-y" /> </div> </div> </div> ``` 部分样式: ``` <style scoped="scoped" lang="scss"> .img-waper { margin-right: 10px; margin-bottom: 10px; position: relative; .remove-icon:hover { cursor: pointer; img { opacity: 0.6; } } .remove-icon { position: absolute; width: 14px; height: 14px; top: -8px; right: -5px; img { width: 100%; } } } </style> ``` #### ts部分 这里图片的多图是直接循环一张一张传的,也可以直接一次批量上传了,因为这里最多只能传几张,在加上时间紧急就先弄成一张一张的传递 ``` <script setup lang="ts" name="upLoadLabData"> import { defineAsyncComponent, reactive, nextTick, onMounted, toRefs, ref } from 'vue' import { ElMessageBox, ElMessage, genFileId } from 'element-plus' import { getToken } from '/@/utils/auth' const inputImgFile = ref() // 用于触发input的点击事件,这样就会触发input[tyle='file']的文件选择事件 const addImgClick = () => { inputImgFile.value.click() } const inputImgFileClick = () => {} // 当有文件选择的时候执行,input里边的事件绑定了的 @change="uploadimg" const uploadimg = (e: any) => { const imgFileExtensionArry = ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp', 'psd', 'svg', 'tiff', 'mp4', 'ogg'] let imgCount = e.target.files.length if (imgCount > 9) { ElMessage.warning('不允许超过9张图片,本次选择超过了9张图片,请重新选择!') return } let haveImgCount = state.UpLoadData.imgJson.length if (haveImgCount + imgCount > 9) { ElMessage.error('不允许超过9张图片,已经上传了' + haveImgCount + '张图片,还能上传' + (9 - haveImgCount) + '张图片') return } for (let index = 0; index < e.target.files.length; index++) { const file = e.target.files[index] const fileExt = file.name.substr(file.name.lastIndexOf('.') + 1) if (imgFileExtensionArry.indexOf(fileExt.toLowerCase()) === -1) { ElMessage.warning('仅允许提交图片与视频文件!') return } // 构建上传图片的FormData const formData = new FormData() formData.append('bucketName', 'teacher-certification') formData.append('filePath', 'clients') formData.append('FileType', '1') // 重点是这个,其他都是一些额外参数,比如bucketName是文件存储桶的名称,FileType是后台存储一个文件类型等 formData.append('file', file) if (state.UpLoadData.stuImgs && state.UpLoadData.imgJson.length === 9) { ElMessage.error('不允许超过9张图片') return } upLoadImg(formData) } } // 封装上传图片的方法,这里用的原生的fetch方法做请求,也可以自己换成封装的请求方法 const upLoadImg = async (formData: FormData) => { fetch('/oss/api/TnblogFiles/UpLoadFormFile', { method: 'POST', headers: { // 'Content-Type': 'application/json', Authorization: 'Bearer ' + getToken() // 替换为你的 token }, body: formData }) .then((response) => { if (response.ok) { return response.json() // 转换为JSON } throw new Error('Network response was not ok.') }) .then((response: any) => { console.log('上传图片成功...', response) // 其他逻辑,实现图片预览,视频预览或者构建上传参数等 // state.UpLoadData.Sign = response.data.id }) .catch((error) => { console.error('上传失败', error) }) } </script> ``` 封装上传图片的方法,用的原生的fetch方法做请求,也可以自己换成封装的请求方法,注意原生的fetch想要获取返回值还要在写一个then里边使用tojson才行 ### 包含上传后的图片预览和视频预览以及相关的其他操作 #### 效果如下 <img src="https://img.tnblog.net/arcimg/aojiancc2/0115d7fb7b9f449284e51e0ac009da53.png" style="width:566px;height:auto;"> #### 整体的代码如下 里边要注意上传类型有两种类型也就是视频和图片所以处理预览的时候要注意,要根据不同的类型去处理,而且图片预览还要注意处理索引的问题,点击某个图片就要从某个图片开始 ``` <template> <div class="upLoadLabData-container"> <div> <div class="lab-from-item"> <div class="lable"><span class="is-required">*</span>关键信息(截图与视频)</div> <div class="form-content panle"> <div class="img-boxs"> <div v-for="(item, i) in state.UpLoadData.imgJson" :key="i + 'img'" class="img-waper"> <div v-if="parseFile(item)" @click="preViewVedioWay(item.fileUrl)" class="vedioWrap" style="cursor: pointer; position: relative; width: 100px; height: 100px" > <div class="el-icon-video-play-wrap"> <el-icon class="el-icon-video-play-my"><VideoPlay /></el-icon> </div> <video id="myVideo" width="100px" class="video-element" height="100px"> <source :src="item.fileUrl" type="video/mp4" /> <source :src="item.fileUrl" type="video/ogg" /> 您的浏览器不支持视频标签。 </video> <div class="video-overlay" /> </div> <img v-else :src="'/oss/api/ImageViewer/' + item.id + '.jpg?percent=0.6&quality=80&od=true'" @click="onPreviewImg(i)" style="cursor: pointer" width="100px" height="100px" /> <div class="remove-icon" @click="rmImgs(i)"> <img src="/imgs/error.png" /> </div> </div> <div class="img-waper"> <div class="add-btn" @click="addImgClick"> <input ref="inputImgFile" @change="uploadimg" @click.stop="inputImgFileClick" type="file" multiple="true" style="display: none" accept="image/*,video/*" /> <div class="icon-waper"> <div class="icon-x" /> <div class="icon-y" /> </div> </div> </div> </div> </div> </div> </div> <!-- 上传图片的图片预览 --> <el-image-viewer v-if="state.upShowViewer" :z-index="20000" :initial-index="state.upInitialIndex" :append-to-body="true" @close="upCloseViewer" :mask-closable="false" :url-list="state.upLoadImageViews" /> <!-- 视频预览 --> <el-dialog v-model="state.preViewDialogVisible" title="视频预览" width="840px"> <template #title> <div style="font-size: 12px; color: #393939">视频预览</div> </template> <div style="margin-top: 0px"> <video ref="preViewVedioEl" width="800px" height="100%" autoplay controls> <source :src="state.preViewVedioUrl" type="video/mp4" /> <source :src="state.preViewVedioUrl" type="video/ogg" /> 您的浏览器不支持视频标签。 </video> </div> </el-dialog> </div> </template> <script setup lang="ts" name="upLoadLabData"> import { defineAsyncComponent, reactive, nextTick, onMounted, ref } from 'vue' import { ElMessageBox, ElMessage } from 'element-plus' import { getLastFileExtension } from '/@/utils/toolsFunctions' import { getToken } from '/@/utils/auth' import request from '/@/utils/requestTools' import { VideoPlay } from '@element-plus/icons-vue' const props = defineProps({ subTrainingProgramId: String, userResultData: {} as any }) const state = reactive({ fileName: '', UpLoadData: { stuImgs: '', imgJson: [] } as any, preViewDialogVisible: false, preViewVedioUrl: '', isShowFileList: true, upShowViewer: false, upInitialIndex: 0, fileList: [], upLoadImageViews: [] }) onMounted(() => { if (props.userResultData) { state.fileList.push({ name: props.userResultData.upLoadDataName, upLoadData: props.userResultData.upLoadData }) } }) const inputImgFile = ref() // 用于触发input的点击事件,这样就会触发input[tyle='file']的文件选择事件 const addImgClick = () => { inputImgFile.value.click() } const inputImgFileClick = () => {} // 当有文件选择的时候执行,input里边的事件绑定了的 @change="uploadimg" const uploadimg = (e: any) => { const imgFileExtensionArry = ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp', 'psd', 'svg', 'tiff', 'mp4', 'ogg'] let imgCount = e.target.files.length if (imgCount > 9) { ElMessage.warning('不允许超过9张图片,本次选择超过了9张图片,请重新选择!') return } let haveImgCount = state.UpLoadData.imgJson.length if (haveImgCount + imgCount > 9) { ElMessage.error('不允许超过9张图片,已经上传了' + haveImgCount + '张图片,还能上传' + (9 - haveImgCount) + '张图片') return } for (let index = 0; index < e.target.files.length; index++) { const file = e.target.files[index] const fileExt = file.name.substr(file.name.lastIndexOf('.') + 1) if (imgFileExtensionArry.indexOf(fileExt.toLowerCase()) === -1) { ElMessage.warning('仅允许提交图片与视频文件!') return } // 构建上传图片的FormData const formData = new FormData() formData.append('bucketName', 'teacher-certification') formData.append('filePath', 'clients') formData.append('FileType', '1') formData.append('file', file) // 重点是这个,其他都是一些额外参数,比如bucketName是文件存储桶的名称,FileType是后台存储一个文件类型等 if (state.UpLoadData.stuImgs && state.UpLoadData.imgJson.length === 9) { ElMessage.error('不允许超过9张图片') return } upLoadImg(formData, fileExt) } } // 封装上传图片的接口,这里用的原生的fetch方法做请求,也可以自己换成封装的请求方法 const upLoadImg = async (formData: FormData, fileExt: string) => { fetch('/oss/api/TnblogFiles/UpLoadFormFile', { method: 'POST', headers: { // 'Content-Type': 'application/json', Authorization: 'Bearer ' + getToken() // 替换为你的 token }, body: formData }) .then((response) => { if (response.ok) { return response.json() // 转换为JSON } throw new Error('Network response was not ok.') }) .then((res: any) => { console.log('上传图片成功...', res) const imgInfo = { id: res.data.id, fileName: res.data.title, fileUrl: '/oss/api/ImageViewer/' + res.data.id + '.jpg' } state.UpLoadData.imgJson.push(imgInfo) // 放到图片预览里边 if (fileExt != 'mp4' && fileExt != 'egg') { state.upLoadImageViews.push(imgInfo.fileUrl) } // 其他数据的构建,比如存储回传的构建 // state.UpLoadData.Sign = response.data.id }) .catch((error) => { console.error('上传失败', error) }) } // 检查文件的后缀,如果是视频就需要单独处理 const parseFile = (item: any) => { let extension = getLastFileExtension(item.fileName) extension = extension.toLowerCase() if (extension == 'mp4' || extension == 'ogg') { parseVedioUrl(item) return true } else { return false } } const parseVedioUrl = (item: any) => { request.get('/oss/api/TnblogFiles/GetFileUrl', { fileId: item.id }).then((res: any) => { item.fileUrl = res.data.url }) } const rmImgs = (i: Number) => { state.UpLoadData.imgJson.splice(i, 1) } const onPreviewImg = (i: number) => { // 找到当前图片索引前面的视频数量,图片预览的时候要减去这个数量才对 // 或者根据图片的id去在图片预览列表里边去找到他在那个索引里边 let poi = 0 for (let index = 0; index < state.UpLoadData.imgJson.length; index++) { if (i > index) { const element = state.UpLoadData.imgJson[index] let extension = getLastFileExtension(element.fileName) if (extension == 'mp4' || extension == 'ogg') { poi = poi + 1 } } } //设置预览图片的索引 state.upInitialIndex = i - poi state.upShowViewer = true } const preViewVedioEl = ref() const preViewVedioWay = (_preViewVedioUrl: string) => { state.preViewDialogVisible = true state.preViewVedioUrl = _preViewVedioUrl // video标签并不会自动重新加载新的视频流,所以我们需要手动调用load()方法来重新加载新的视频源 preViewVedioEl.value.load() // 自动播放,可以根据情况来判断是否调用 preViewVedioEl.value.play() } const upCloseViewer = () => { state.upShowViewer = false } </script> <style scoped="scoped" lang="scss"> .upLoadLabData-container { padding: 30px 35px 30px 35px; .upLoadToServer { margin-top: 26px; display: flex; justify-content: flex-end; } .el-upload { width: 100%; } } </style> <style lang="scss"> .upLoadLabData-container { .el-upload { width: 100%; } } </style> <style scoped="scoped" lang="scss"> // 显示为视频这块效果,指上去播放的效果放大 .vedioWrap:hover { .el-icon-video-play-wrap { left: 33px; top: 33px; } .el-icon-video-play-my { // color:#ff5555; font-size: 32px; } } .el-icon-video-play-wrap { position: absolute; left: 36px; top: 36px; z-index: 999; } .el-icon-video-play-my { color: #fff; font-size: 26px; } .video-overlay { position: absolute; width: 100%; height: 100%; top: 0; left: 0; background-color: rgba(0, 0, 0, 0.4); /* 半透明遮罩层 */ } .lab-from-item { .lable { font-size: 14px; font-family: Microsoft YaHei; font-weight: 400; color: #121212; padding: 5px 0; .is-required { color: #e74c3c; } } .form-content { .file-select-box { border-radius: 0; .el-input-group__append, .el-input__inner { border-radius: 0; border-color: #dcdfe6; } .el-input-group__append { background-color: #2b56a5; color: #f8f8f8; } } } .form-content.panle { padding: 20px; padding-bottom: 10px; background-color: #f8f8f8; .img-boxs { display: flex; /* margin-right: 46px; */ flex-wrap: wrap; .img-waper { margin-right: 10px; margin-bottom: 10px; position: relative; .remove-icon:hover { cursor: pointer; img { opacity: 0.6; } } .remove-icon { position: absolute; width: 14px; height: 14px; top: -8px; right: -5px; img { width: 100%; } } } } } } .add-btn:hover { cursor: pointer; opacity: 0.6; } .add-btn { width: 70px; height: 70px; border: solid 1px #dddddd; display: flex; align-items: center; flex-wrap: wrap; justify-content: space-around; .icon-waper { width: 18px; height: 21px; position: relative; .icon-x { width: 19px; height: 5px; background-color: #dddddd; position: absolute; top: 0; bottom: 0; margin: auto; } .icon-y { height: 21px; width: 5px; background-color: #dddddd; position: absolute; left: 0; right: 0; margin: auto; } } } </style> ``` toolsFunctions里边封装的方法也贴一下: ``` export function getLastFileExtension(filename: String) { // 查找最后一个点的位置 var lastDotIndex = filename.lastIndexOf('.'); // 如果没有点,则返回整个字符串作为后缀 if (lastDotIndex === -1) { return filename; } // 返回从最后一个点之后开始的字符串 return filename.substring(lastDotIndex + 1); } ```