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

实现多路视频截图预览之后上传到后台系统

********************父组件**********************

<div class="camera-box" v-loading="i.loading">

                            <div

                                class="camera-box-inner"

                                v-for="(x, y) in i.children"

                                :key="y + 'children' + x.featureCode"

                                v-show="x.isShow"

                                :style="`width: ${i.videoInnerStyle!.width + '%'};max-height: ${i.videoInnerStyle!.height + '%'

                                    };`">

                                <div class="userName-box" v-if="!i.isApp">

                                    {{ `${i.userName} — ${x.cameraName || i.cameraName}` }}

                                </div>

                                <div class="userName-box" v-else>{{ `${i.userName}` }}</div>

                                <video

                                    :class="{ appVideo: i.isApp }"

                                    :id="x.featureCode"

                                    muted

                                    autoplay

                                    controls

                                    v-show="x.camera"

                                    :style="x.PhotoShow ? { border: '2px solid red' } : {}"></video>

                                <Photo

                                    v-if="x.PhotoShow"

                                    :video="x.videoEl"

                                    :userName="i.userName"

                                    @close="photoClose(x)" />

                                <div class="takePhoto cusp iconfont icon-a-commonphoto" @click="takePhoto(x)"></div>

                            </div>

                        </div>

// 截图

function takePhoto(data) {

    const videoEl = document.getElementById(data.featureCode) as HTMLVideoElement | null

    if (videoEl) {

        data.PhotoShow = true

        data.videoEl = videoEl

    }

}

// 截图预览关闭

function photoClose(data) {

    data.PhotoShow = false

}

********************子组件***********************

<template>

    <div class="canvas-photo">

        <canvas ref="photoCanvas"></canvas>

        <el-dialog

            v-model="previewDialog.show"

            :title="previewDialog.title"

            width="600px"

            @close="handlePreviewClose"

            append-to-body>

            <div>

                <img :src="previewDialog.imageUrl" alt="" />

            </div>

            <template #footer>

                <el-button @click="previewDialog.show = false">取消</el-button>

                <el-button type="primary" @click="handleConfirmUpload">确定上传</el-button>

            </template>

        </el-dialog>

    </div>

</template>

<script setup lang="ts">

import { onMounted, PropType, ref } from 'vue'

import request from '../../../utils/request'

import { Session } from '../../../utils/storage'

import { formatTime, base64ToFile } from '/@/utils'

import { ElNotification, ElForm, ElFormItem, ElProgress } from 'element-plus'

const props = defineProps({

    video: {

        type: Object as PropType<HTMLVideoElement>,

        required: true

    },

    userName: {

        type: String as PropType<string>,

        default: ''

    }

})

const emit = defineEmits(['close'])

const fileName = ref<string>('')

const canvas = ref<any>()

const imgFile = ref<any>()

const previewDialog = reactive<{

    show: boolean

    title: string

    imageUrl: string

}>({

    show: false,

    title: '预览截图',

    imageUrl: ''

})

const photoCanvas = ref()

const ctx = ref<any>()

// // 下载图片

// function downloadCanvas() {

//  const link = document.createElement('a')

//  link.download = `用户-${props.userName}-视频截图${formatTime(new Date().getTime(), 'yyyy-mm-dd hh-MM-ss')}.png`

//  link.href = photoCanvas.value.toDataURL('image/png')

//  link.click()

// }

const upLoadProgress = ref<number>(0)

// 截图本地保存和上传到文件管理

const captureAndSaveFrame = () => {

    const video = props.video

    if (!video || !photoCanvas.value) return

    const fileName = `用户-${props.userName}-视频截图${formatTime(new Date().getTime(), 'yyyy-mm-dd hh-MM-ss')}.png`

    // 创建canvas元素

    canvas.value = document.createElement('canvas')

    const ctx = canvas.value.getContext('2d')!

    // 设置canvas尺寸与视频一致

    canvas.value.width = video.videoWidth

    canvas.value.height = video.videoHeight

    // 将视频帧绘制到canvas

    ctx.drawImage(video, 0, 0, canvas.value.width, canvas.value.height)

    // 生成图片数据URL(支持PNG格式)

    const imageDataUrl = canvas.value.toDataURL('image/png')

    const link = document.createElement('a')

    link.href = imageDataUrl

    const imageName = `${fileName}_${Date.now()}.png`

    link.download = imageName

    // base64转为file文件

    imgFile.value = base64ToFile(imageDataUrl, imageName)

    // 模拟点击下载

    document.body.appendChild(link)

    link.click()

    document.body.removeChild(link)

    // 更新预览

    previewDialog.imageUrl = imageDataUrl

    previewDialog.show = true

}

const handleConfirmUpload = () => {

    // 上传到服务器

    const formData = new FormData()

    // 将文件添加到 FormData 中,以便后续发送请求

    formData.append('file', imgFile.value)

    // 添加额外的请求参数,这里 dir 为空字符串

    formData.append('dir', '')

    // 添加额外的请求参数,这里 type 为 10

    formData.append('type', '10')

    ElNotification({

        type: 'info',

        title: '一个截图文件正在上传',

        dangerouslyUseHTMLString: true,

        message: h(ElForm, { model: {}, 'label-width': '80px' }, [

            h(ElFormItem, { label: '文件名:' }, fileName.value),

            h(ElFormItem, { label: '上传进度:' }, [

                h(ElProgress, {

                    id: 'meetingRoomUploadProgress',

                    percentage: 0,

                    style: {

                        width: '200px'

                    }

                })

            ])

        ]),

        showClose: true,

        duration: 0

    })

    const el = document

        .getElementById('meetingRoomUploadProgress')

        ?.getElementsByClassName('el-progress-bar__inner')[0] as HTMLElement

    const elText = document

        .getElementById('meetingRoomUploadProgress')

        ?.getElementsByClassName('el-progress__text')[0]

        .getElementsByTagName('span')[0] as HTMLElement

    request('/admin/sys-file/upload', {

        method: 'POST',

        headers: {

            'Content-Type': 'multipart/form-data',

            Authorization: 'Bearer ' + Session.get('token'),

            'TENANT-ID': Session.getTenant()

        },

        onUploadProgress: (progressEvent: any) => {

            upLoadProgress.value = Number(progressEvent.progress.toFixed(2)) * 100

            if (upLoadProgress.value === 100) {

                el.style.width = '100%'

                elText.innerHTML = '100%'

                setTimeout(() => {

                    ElNotification.closeAll()

                }, 1000)

            } else {

                el.style.width = upLoadProgress.value + '%'

                elText.innerHTML = upLoadProgress.value + '%'

            }

        },

        data: formData

    })

        .then(response => {

            if (!response.ok) {

                throw new Error('Network response was not ok')

            }

            return response.json()

        })

        .then(data => {

            // 请求成功时的处理,打印返回的数据

            console.log('success', data)

        })

        .catch(err => {

            // 请求失败时的处理,打印错误信息

            console.log('error', err)

        })

    previewDialog.show = false

    // 清理资源

    cleanupCanvas()

}

// 清理canvas

const cleanupCanvas = () => {

    if (canvas.value) {

        canvas.value.remove()

        canvas.value = null

    }

}

const handlePreviewClose = () => {

    emit('close')

    cleanupCanvas()

}

// 更新canvas尺寸

const updateCanvasSize = () => {

    if (!photoCanvas.value || !props.video) return

    const width = photoCanvas.value.offsetWidth

    const height = photoCanvas.value.offsetHeight

    photoCanvas.value.width = width

    photoCanvas.value.height = height

    if (ctx.value && props.video.videoWidth) {

        ctx.value.drawImage(props.video, 0, 0, width, height)

    }

}

onMounted(() => {

    if (!photoCanvas.value) return

    ctx.value = photoCanvas.value.getContext('2d')

    // 等待DOM渲染完成后执行

    nextTick(() => {

        updateCanvasSize()

        // 初始捕获一帧

        captureAndSaveFrame()

    })

    // 监听窗口大小变化

    window.addEventListener('resize', updateCanvasSize)

})

onUnmounted(() => {

    // 移除事件监听器

    window.removeEventListener('resize', updateCanvasSize)

    // 清理资源

    cleanupCanvas()

    // 关闭所有通知

    ElNotification.closeAll()

})

</script>

<style scoped lang="scss">

.canvas-photo {

    width: 100%;

    height: 100%;

    position: absolute;

    top: 0;

    left: 0;

    opacity: 0;

    z-index: -10;

    canvas {

        width: 100%;

        height: 100%;

    }

    .preview-container {

        padding: 20px;

        text-align: center;

        .preview-image {

            max-width: 100%;

            max-height: calc(50vh - 100px);

            border: 1px solid #ebeef5;

            border-radius: 4px;

            box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);

        }

    }

}

</style>

**********工具函数********************

/**

 * 将Base64字符串转换为File对象

 * @param {string} base64 - Base64编码的字符串

 * @param {string} filename - 生成文件的名称

 * @param {string} [mimeType] - 文件的MIME类型,默认为'image/png'

 * @returns {File} - 返回的File对象

 */

export function base64ToFile(base64, filename, mimeType = 'image/png') {

    // 1. 移除Base64前缀(如果有)

    const base64WithoutPrefix = base64.replace(/^data:.+;base64,/, '')

    // 2. 将Base64字符串转换为字节数组

    const byteCharacters = atob(base64WithoutPrefix)

    const byteArrays = []

    for (let offset = 0; offset < byteCharacters.length; offset += 512) {

        const slice = byteCharacters.slice(offset, offset + 512)

        const byteNumbers = new Array(slice.length)

        for (let i = 0; i < slice.length; i++) {

            byteNumbers[i] = slice.charCodeAt(i)

        }

        const byteArray = new Uint8Array(byteNumbers)

        byteArrays.push(byteArray)

    }

    // 3. 创建Blob对象

    const blob = new Blob(byteArrays, { type: mimeType })

    // 4. 将Blob转换为File对象

    return new File([blob], filename, { type: mimeType })

}

实现效果图

http://www.xdnf.cn/news/970075.html

相关文章:

  • 2025年ASOC SCI2区TOP,协同搜索框架自适应算法+多无人机巡检规划,深度解析+性能实测
  • 专题一_双指针_复写零
  • HDFS 3.4.1 集成Kerberos 实现账户认证
  • 驭码CodeRider 2.0深度测评:助力高效开发【探索化学奇妙世界】网站
  • 【靶场】xxe漏洞2
  • 黑马Mybatis
  • UE5 学习系列(三)创建和移动物体
  • MySQL事务——博主总结
  • C# Serilog 日志
  • 西电计组第四章-存储系统
  • 72道Nginx高频题整理(附答案背诵版)
  • 【Qt】显示类控件 QLabel、QLCDNumer、QProgressBar、QCalendarWidget
  • ROS-编写工作区、功能包、节点
  • 通过Elastic EDR看smbexec并进行二次开发Bypass
  • @component、@bean、@Configuration的区别
  • 在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能
  • MySQL:InnoDB架构(内存架构篇)
  • Grey任命李文杰为中国总裁,开启增长新章
  • 云服务运行安全创新标杆:阿里云飞天洛神云网络子系统“齐天”再次斩获奖项
  • 12要素法:构建高效云原生应用
  • 鸿蒙Next仓颉语言开发实战教程:下拉刷新和上拉加载更多
  • leetcode:42. 接雨水(秒变简单题)
  • 代码训练LeetCode(27)接雨水
  • 【PX4飞控】右手坐标系与右手系旋转正方向的定义与判断方法
  • go全局配置redis,全局只需要连接一次,然后全局可以引用使用
  • UVa12298 3KP-BASH Project
  • Codeforces Round 1027 (Div. 3)-G
  • Oracle 数据库对象管理:表空间与表的操作
  • 解决克隆Github源码库时的Permission denied 问题
  • 入门学者做的excel文献笔记发现不了问题怎么办?——用提示词来解决