应无所住,而生其心
排名
1
文章
860
粉丝
112
评论
163
net core webapi post传递参数
庸人 : 确实坑哈,我也是下班好了好几次,发现后台传递对象是可以的,但...
百度编辑器自定义模板
庸人 : 我建议换个编辑器,因为现在百度富文本已经停止维护了,用tinymec...
ICP备案 :渝ICP备18016597号-1
网站信息:2018-2025TNBLOG.NET
技术交流:群号656732739
联系我们:contact@tnblog.net
公网安备:50010702506256
欢迎加群交流技术

vue3 图片上传,文件上传,视频上传。图片预览,视频预览。

734人阅读 2024/12/9 18:31 总访问:5182262 评论:0 收藏:0 手机
分类: 前端


直接调用接口上传参考:https://www.tnblog.net/notebook/article/details/8495

基础的图片上传,文件上传,视频上传

界面就是这样的:

界面当中的部分代码如下

  1. <div class="img-waper">
  2. <div class="add-btn" @click="addImgClick">
  3. <input
  4. ref="inputImgFile"
  5. @change="uploadimg"
  6. @click.stop="inputImgFileClick"
  7. type="file"
  8. multiple="true"
  9. style="display: none"
  10. accept="image/*,video/*"
  11. />
  12. <div class="icon-waper">
  13. <div class="icon-x" />
  14. <div class="icon-y" />
  15. </div>
  16. </div>
  17. </div>

部分样式:

  1. <style scoped="scoped" lang="scss">
  2. .img-waper {
  3. margin-right: 10px;
  4. margin-bottom: 10px;
  5. position: relative;
  6. .remove-icon:hover {
  7. cursor: pointer;
  8. img {
  9. opacity: 0.6;
  10. }
  11. }
  12. .remove-icon {
  13. position: absolute;
  14. width: 14px;
  15. height: 14px;
  16. top: -8px;
  17. right: -5px;
  18. img {
  19. width: 100%;
  20. }
  21. }
  22. }
  23. </style>

ts部分

这里图片的多图是直接循环一张一张传的,也可以直接一次批量上传了,因为这里最多只能传几张,在加上时间紧急就先弄成一张一张的传递

  1. <script setup lang="ts" name="upLoadLabData">
  2. import { defineAsyncComponent, reactive, nextTick, onMounted, toRefs, ref } from 'vue'
  3. import { ElMessageBox, ElMessage, genFileId } from 'element-plus'
  4. import { getToken } from '/@/utils/auth'
  5. const inputImgFile = ref()
  6. // 用于触发input的点击事件,这样就会触发input[tyle='file']的文件选择事件
  7. const addImgClick = () => {
  8. inputImgFile.value.click()
  9. }
  10. const inputImgFileClick = () => {}
  11. // 当有文件选择的时候执行,input里边的事件绑定了的 @change="uploadimg"
  12. const uploadimg = (e: any) => {
  13. const imgFileExtensionArry = ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp', 'psd', 'svg', 'tiff', 'mp4', 'ogg']
  14. let imgCount = e.target.files.length
  15. if (imgCount > 9) {
  16. ElMessage.warning('不允许超过9张图片,本次选择超过了9张图片,请重新选择!')
  17. return
  18. }
  19. let haveImgCount = state.UpLoadData.imgJson.length
  20. if (haveImgCount + imgCount > 9) {
  21. ElMessage.error('不允许超过9张图片,已经上传了' + haveImgCount + '张图片,还能上传' + (9 - haveImgCount) + '张图片')
  22. return
  23. }
  24. for (let index = 0; index < e.target.files.length; index++) {
  25. const file = e.target.files[index]
  26. const fileExt = file.name.substr(file.name.lastIndexOf('.') + 1)
  27. if (imgFileExtensionArry.indexOf(fileExt.toLowerCase()) === -1) {
  28. ElMessage.warning('仅允许提交图片与视频文件!')
  29. return
  30. }
  31. // 构建上传图片的FormData
  32. const formData = new FormData()
  33. formData.append('bucketName', 'teacher-certification')
  34. formData.append('filePath', 'clients')
  35. formData.append('FileType', '1')
  36. // 重点是这个,其他都是一些额外参数,比如bucketName是文件存储桶的名称,FileType是后台存储一个文件类型等
  37. formData.append('file', file)
  38. if (state.UpLoadData.stuImgs && state.UpLoadData.imgJson.length === 9) {
  39. ElMessage.error('不允许超过9张图片')
  40. return
  41. }
  42. upLoadImg(formData)
  43. }
  44. }
  45. // 封装上传图片的方法,这里用的原生的fetch方法做请求,也可以自己换成封装的请求方法
  46. const upLoadImg = async (formData: FormData) => {
  47. fetch('/oss/api/TnblogFiles/UpLoadFormFile', {
  48. method: 'POST',
  49. headers: {
  50. // 'Content-Type': 'application/json',
  51. Authorization: 'Bearer ' + getToken() // 替换为你的 token
  52. },
  53. body: formData
  54. })
  55. .then((response) => {
  56. if (response.ok) {
  57. return response.json() // 转换为JSON
  58. }
  59. throw new Error('Network response was not ok.')
  60. })
  61. .then((response: any) => {
  62. console.log('上传图片成功...', response)
  63. // 其他逻辑,实现图片预览,视频预览或者构建上传参数等
  64. // state.UpLoadData.Sign = response.data.id
  65. })
  66. .catch((error) => {
  67. console.error('上传失败', error)
  68. })
  69. }
  70. </script>

封装上传图片的方法,用的原生的fetch方法做请求,也可以自己换成封装的请求方法,注意原生的fetch想要获取返回值还要在写一个then里边使用tojson才行

包含上传后的图片预览和视频预览以及相关的其他操作

效果如下

整体的代码如下

里边要注意上传类型有两种类型也就是视频和图片所以处理预览的时候要注意,要根据不同的类型去处理,而且图片预览还要注意处理索引的问题,点击某个图片就要从某个图片开始

  1. <template>
  2. <div class="upLoadLabData-container">
  3. <div>
  4. <div class="lab-from-item">
  5. <div class="lable"><span class="is-required">*</span>关键信息(截图与视频)</div>
  6. <div class="form-content panle">
  7. <div class="img-boxs">
  8. <div v-for="(item, i) in state.UpLoadData.imgJson" :key="i + 'img'" class="img-waper">
  9. <div
  10. v-if="parseFile(item)"
  11. @click="preViewVedioWay(item.fileUrl)"
  12. class="vedioWrap"
  13. style="cursor: pointer; position: relative; width: 100px; height: 100px"
  14. >
  15. <div class="el-icon-video-play-wrap">
  16. <el-icon class="el-icon-video-play-my"><VideoPlay /></el-icon>
  17. </div>
  18. <video id="myVideo" width="100px" class="video-element" height="100px">
  19. <source :src="item.fileUrl" type="video/mp4" />
  20. <source :src="item.fileUrl" type="video/ogg" />
  21. 您的浏览器不支持视频标签。
  22. </video>
  23. <div class="video-overlay" />
  24. </div>
  25. <img
  26. v-else
  27. :src="'/oss/api/ImageViewer/' + item.id + '.jpg?percent=0.6&quality=80&od=true'"
  28. @click="onPreviewImg(i)"
  29. style="cursor: pointer"
  30. width="100px"
  31. height="100px"
  32. />
  33. <div class="remove-icon" @click="rmImgs(i)">
  34. <img src="/imgs/error.png" />
  35. </div>
  36. </div>
  37. <div class="img-waper">
  38. <div class="add-btn" @click="addImgClick">
  39. <input
  40. ref="inputImgFile"
  41. @change="uploadimg"
  42. @click.stop="inputImgFileClick"
  43. type="file"
  44. multiple="true"
  45. style="display: none"
  46. accept="image/*,video/*"
  47. />
  48. <div class="icon-waper">
  49. <div class="icon-x" />
  50. <div class="icon-y" />
  51. </div>
  52. </div>
  53. </div>
  54. </div>
  55. </div>
  56. </div>
  57. </div>
  58. <!-- 上传图片的图片预览 -->
  59. <el-image-viewer
  60. v-if="state.upShowViewer"
  61. :z-index="20000"
  62. :initial-index="state.upInitialIndex"
  63. :append-to-body="true"
  64. @close="upCloseViewer"
  65. :mask-closable="false"
  66. :url-list="state.upLoadImageViews"
  67. />
  68. <!-- 视频预览 -->
  69. <el-dialog v-model="state.preViewDialogVisible" title="视频预览" width="840px">
  70. <template #title>
  71. <div style="font-size: 12px; color: #393939">视频预览</div>
  72. </template>
  73. <div style="margin-top: 0px">
  74. <video ref="preViewVedioEl" width="800px" height="100%" autoplay controls>
  75. <source :src="state.preViewVedioUrl" type="video/mp4" />
  76. <source :src="state.preViewVedioUrl" type="video/ogg" />
  77. 您的浏览器不支持视频标签。
  78. </video>
  79. </div>
  80. </el-dialog>
  81. </div>
  82. </template>
  83. <script setup lang="ts" name="upLoadLabData">
  84. import { defineAsyncComponent, reactive, nextTick, onMounted, ref } from 'vue'
  85. import { ElMessageBox, ElMessage } from 'element-plus'
  86. import { getLastFileExtension } from '/@/utils/toolsFunctions'
  87. import { getToken } from '/@/utils/auth'
  88. import request from '/@/utils/requestTools'
  89. import { VideoPlay } from '@element-plus/icons-vue'
  90. const props = defineProps({
  91. subTrainingProgramId: String,
  92. userResultData: {} as any
  93. })
  94. const state = reactive({
  95. fileName: '',
  96. UpLoadData: {
  97. stuImgs: '',
  98. imgJson: []
  99. } as any,
  100. preViewDialogVisible: false,
  101. preViewVedioUrl: '',
  102. isShowFileList: true,
  103. upShowViewer: false,
  104. upInitialIndex: 0,
  105. fileList: [],
  106. upLoadImageViews: []
  107. })
  108. onMounted(() => {
  109. if (props.userResultData) {
  110. state.fileList.push({
  111. name: props.userResultData.upLoadDataName,
  112. upLoadData: props.userResultData.upLoadData
  113. })
  114. }
  115. })
  116. const inputImgFile = ref()
  117. // 用于触发input的点击事件,这样就会触发input[tyle='file']的文件选择事件
  118. const addImgClick = () => {
  119. inputImgFile.value.click()
  120. }
  121. const inputImgFileClick = () => {}
  122. // 当有文件选择的时候执行,input里边的事件绑定了的 @change="uploadimg"
  123. const uploadimg = (e: any) => {
  124. const imgFileExtensionArry = ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp', 'psd', 'svg', 'tiff', 'mp4', 'ogg']
  125. let imgCount = e.target.files.length
  126. if (imgCount > 9) {
  127. ElMessage.warning('不允许超过9张图片,本次选择超过了9张图片,请重新选择!')
  128. return
  129. }
  130. let haveImgCount = state.UpLoadData.imgJson.length
  131. if (haveImgCount + imgCount > 9) {
  132. ElMessage.error('不允许超过9张图片,已经上传了' + haveImgCount + '张图片,还能上传' + (9 - haveImgCount) + '张图片')
  133. return
  134. }
  135. for (let index = 0; index < e.target.files.length; index++) {
  136. const file = e.target.files[index]
  137. const fileExt = file.name.substr(file.name.lastIndexOf('.') + 1)
  138. if (imgFileExtensionArry.indexOf(fileExt.toLowerCase()) === -1) {
  139. ElMessage.warning('仅允许提交图片与视频文件!')
  140. return
  141. }
  142. // 构建上传图片的FormData
  143. const formData = new FormData()
  144. formData.append('bucketName', 'teacher-certification')
  145. formData.append('filePath', 'clients')
  146. formData.append('FileType', '1')
  147. formData.append('file', file) // 重点是这个,其他都是一些额外参数,比如bucketName是文件存储桶的名称,FileType是后台存储一个文件类型等
  148. if (state.UpLoadData.stuImgs && state.UpLoadData.imgJson.length === 9) {
  149. ElMessage.error('不允许超过9张图片')
  150. return
  151. }
  152. upLoadImg(formData, fileExt)
  153. }
  154. }
  155. // 封装上传图片的接口,这里用的原生的fetch方法做请求,也可以自己换成封装的请求方法
  156. const upLoadImg = async (formData: FormData, fileExt: string) => {
  157. fetch('/oss/api/TnblogFiles/UpLoadFormFile', {
  158. method: 'POST',
  159. headers: {
  160. // 'Content-Type': 'application/json',
  161. Authorization: 'Bearer ' + getToken() // 替换为你的 token
  162. },
  163. body: formData
  164. })
  165. .then((response) => {
  166. if (response.ok) {
  167. return response.json() // 转换为JSON
  168. }
  169. throw new Error('Network response was not ok.')
  170. })
  171. .then((res: any) => {
  172. console.log('上传图片成功...', res)
  173. const imgInfo = {
  174. id: res.data.id,
  175. fileName: res.data.title,
  176. fileUrl: '/oss/api/ImageViewer/' + res.data.id + '.jpg'
  177. }
  178. state.UpLoadData.imgJson.push(imgInfo)
  179. // 放到图片预览里边
  180. if (fileExt != 'mp4' && fileExt != 'egg') {
  181. state.upLoadImageViews.push(imgInfo.fileUrl)
  182. }
  183. // 其他数据的构建,比如存储回传的构建
  184. // state.UpLoadData.Sign = response.data.id
  185. })
  186. .catch((error) => {
  187. console.error('上传失败', error)
  188. })
  189. }
  190. // 检查文件的后缀,如果是视频就需要单独处理
  191. const parseFile = (item: any) => {
  192. let extension = getLastFileExtension(item.fileName)
  193. extension = extension.toLowerCase()
  194. if (extension == 'mp4' || extension == 'ogg') {
  195. parseVedioUrl(item)
  196. return true
  197. } else {
  198. return false
  199. }
  200. }
  201. const parseVedioUrl = (item: any) => {
  202. request.get('/oss/api/TnblogFiles/GetFileUrl', { fileId: item.id }).then((res: any) => {
  203. item.fileUrl = res.data.url
  204. })
  205. }
  206. const rmImgs = (i: Number) => {
  207. state.UpLoadData.imgJson.splice(i, 1)
  208. }
  209. const onPreviewImg = (i: number) => {
  210. // 找到当前图片索引前面的视频数量,图片预览的时候要减去这个数量才对
  211. // 或者根据图片的id去在图片预览列表里边去找到他在那个索引里边
  212. let poi = 0
  213. for (let index = 0; index < state.UpLoadData.imgJson.length; index++) {
  214. if (i > index) {
  215. const element = state.UpLoadData.imgJson[index]
  216. let extension = getLastFileExtension(element.fileName)
  217. if (extension == 'mp4' || extension == 'ogg') {
  218. poi = poi + 1
  219. }
  220. }
  221. }
  222. //设置预览图片的索引
  223. state.upInitialIndex = i - poi
  224. state.upShowViewer = true
  225. }
  226. const preViewVedioEl = ref()
  227. const preViewVedioWay = (_preViewVedioUrl: string) => {
  228. state.preViewDialogVisible = true
  229. state.preViewVedioUrl = _preViewVedioUrl
  230. // video标签并不会自动重新加载新的视频流,所以我们需要手动调用load()方法来重新加载新的视频源
  231. preViewVedioEl.value.load()
  232. // 自动播放,可以根据情况来判断是否调用
  233. preViewVedioEl.value.play()
  234. }
  235. const upCloseViewer = () => {
  236. state.upShowViewer = false
  237. }
  238. </script>
  239. <style scoped="scoped" lang="scss">
  240. .upLoadLabData-container {
  241. padding: 30px 35px 30px 35px;
  242. .upLoadToServer {
  243. margin-top: 26px;
  244. display: flex;
  245. justify-content: flex-end;
  246. }
  247. .el-upload {
  248. width: 100%;
  249. }
  250. }
  251. </style>
  252. <style lang="scss">
  253. .upLoadLabData-container {
  254. .el-upload {
  255. width: 100%;
  256. }
  257. }
  258. </style>
  259. <style scoped="scoped" lang="scss">
  260. // 显示为视频这块效果,指上去播放的效果放大
  261. .vedioWrap:hover {
  262. .el-icon-video-play-wrap {
  263. left: 33px;
  264. top: 33px;
  265. }
  266. .el-icon-video-play-my {
  267. // color:#ff5555;
  268. font-size: 32px;
  269. }
  270. }
  271. .el-icon-video-play-wrap {
  272. position: absolute;
  273. left: 36px;
  274. top: 36px;
  275. z-index: 999;
  276. }
  277. .el-icon-video-play-my {
  278. color: #fff;
  279. font-size: 26px;
  280. }
  281. .video-overlay {
  282. position: absolute;
  283. width: 100%;
  284. height: 100%;
  285. top: 0;
  286. left: 0;
  287. background-color: rgba(0, 0, 0, 0.4);
  288. /* 半透明遮罩层 */
  289. }
  290. .lab-from-item {
  291. .lable {
  292. font-size: 14px;
  293. font-family: Microsoft YaHei;
  294. font-weight: 400;
  295. color: #121212;
  296. padding: 5px 0;
  297. .is-required {
  298. color: #e74c3c;
  299. }
  300. }
  301. .form-content {
  302. .file-select-box {
  303. border-radius: 0;
  304. .el-input-group__append,
  305. .el-input__inner {
  306. border-radius: 0;
  307. border-color: #dcdfe6;
  308. }
  309. .el-input-group__append {
  310. background-color: #2b56a5;
  311. color: #f8f8f8;
  312. }
  313. }
  314. }
  315. .form-content.panle {
  316. padding: 20px;
  317. padding-bottom: 10px;
  318. background-color: #f8f8f8;
  319. .img-boxs {
  320. display: flex;
  321. /* margin-right: 46px; */
  322. flex-wrap: wrap;
  323. .img-waper {
  324. margin-right: 10px;
  325. margin-bottom: 10px;
  326. position: relative;
  327. .remove-icon:hover {
  328. cursor: pointer;
  329. img {
  330. opacity: 0.6;
  331. }
  332. }
  333. .remove-icon {
  334. position: absolute;
  335. width: 14px;
  336. height: 14px;
  337. top: -8px;
  338. right: -5px;
  339. img {
  340. width: 100%;
  341. }
  342. }
  343. }
  344. }
  345. }
  346. }
  347. .add-btn:hover {
  348. cursor: pointer;
  349. opacity: 0.6;
  350. }
  351. .add-btn {
  352. width: 70px;
  353. height: 70px;
  354. border: solid 1px #dddddd;
  355. display: flex;
  356. align-items: center;
  357. flex-wrap: wrap;
  358. justify-content: space-around;
  359. .icon-waper {
  360. width: 18px;
  361. height: 21px;
  362. position: relative;
  363. .icon-x {
  364. width: 19px;
  365. height: 5px;
  366. background-color: #dddddd;
  367. position: absolute;
  368. top: 0;
  369. bottom: 0;
  370. margin: auto;
  371. }
  372. .icon-y {
  373. height: 21px;
  374. width: 5px;
  375. background-color: #dddddd;
  376. position: absolute;
  377. left: 0;
  378. right: 0;
  379. margin: auto;
  380. }
  381. }
  382. }
  383. </style>

toolsFunctions里边封装的方法也贴一下:

  1. export function getLastFileExtension(filename: String) {
  2. // 查找最后一个点的位置
  3. var lastDotIndex = filename.lastIndexOf('.');
  4. // 如果没有点,则返回整个字符串作为后缀
  5. if (lastDotIndex === -1) {
  6. return filename;
  7. }
  8. // 返回从最后一个点之后开始的字符串
  9. return filename.substring(lastDotIndex + 1);
  10. }

欢迎加群讨论技术,1群:677373950(满了,可以加,但通过不了),2群:656732739。有需要软件开发,或者学习软件技术的朋友可以和我联系~(Q:815170684)

评价

vue3vue组件props给一个对象参数vue组件间传参数vue父组件给子组件传参数组件参数类型父组件调用子组件的方法vue组件事件监听给子组件传递方法子组件调用父组件方法

[TOC]组件可以使用props给组件传值,可以同时传递多个,可以是任意类型,比如字符串或者对象。 下面是个简单的例子: &lt...

vue3最基础的数据加载表格table

vue3表格加载一点静态数据 &lt;template&gt; &lt;el-table :data=&quot;tableData&quot; style=&quot;width: 100%&quot...

vue3 Element Plus 表单输入框放到一行

当垂直方向空间受限且表单较简单时,可以在一行内放置表单。 通过设置 inline 属性为 true 可以让表单域变为行内的表单域...

vue3 Element ui Plus 表格 分页vue3 el-pagination分页

其实就是el-pagination控件的使用而已 &lt;template&gt; &lt;div&gt; &lt;el-table :data=&quot;tableData&quot; ...

vue触发a标签的点击事件vue3 dom操作 触发点击事件 文件选择库只会触发一次change事件的问题

[TOC]vue触发a标签的点击事件直接操作dom节点的方式比较简单 &lt;button @click=&quot;handleBtnClick&quot;&gt;点击按钮&...

vue3 ref的使用多个ref的使用通过ref触发点击事件

多个ref获取的方法可以使用一样的,通过变量名来区分就行了 const vabUploadRef = ref() const multipleTableRef = ref() ...

vue elementuivue3 element plus 文件上传的时候设置其他参数后台.net接收传递的额外参数图片上传

比如上传文件的时候额外传递两个select选择的值 前台前面上传文件的时候要提供默认参数很简单,el-upload绑定一个data即可...

vuevue3 打开新页面页面跳转vue跳转到一个新页面vue路由传参vue3路由传参vue3 获取路由参数

[TOC]VUE页面跳转本地页面跳转 goApplicationCenter() { //进行页面跳转 let path = &quot;/application-center&quo...

vuevue3组件封装vue组件模板简单组件模板基础组件模板vue引入自定义的组件vue使用自定义的组件插槽slot使用vue封装格子效果一块一块的grid布局效果

[TOC]vue封装组件的简单模板贴一个简单模板方便自定义组件的时候直接复制 &lt;template&gt; &lt;div class=&quot;app...

.net6 Signalr+vue3 的运用(上)

.net6 Signalr+Vue3 的运用(上)[TOC] 什么是 SignalR?ASP.NET Core SignalR 是一个开放源代码库,可用于简化向应用添加...

.net6 Signalr+vue3 的运用(下)

.net6 Signalr+Vue3 的运用(下)[TOC] 上篇链接:https://www.tnblog.net/hb/article/details/7961SignalR 中的用户 Sig...

.net6 Signalr+vue3 配合Ingress Nginx的运用

.net6 Signalr+Vue3 配合Ingress Nginx的运用[TOC] 结合上篇:https://www.tnblog.net/hb/article/details/7963 项目打...

vue3 Element Plus 表格使用vue3常用界面搭配vue3基础模板使用

一个简单的表格加时间搜索界面效果如下: 代码如下: &lt;template&gt; &lt;div class=&quot;app-container&quot;&g...

vue3 如何加prototypevue3使用globalProperties

在2.X版本中创建一个vue 实例是通过 new Vue()来实现的,到了3.X中则是通过使用createApp这个 API返回一个应用实例,并且可...