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

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

<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.pswp.cn/web/83255.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

分布式锁-Redisson实现

目录 本地锁的局限性 Redisson解决分布式锁问题 在分布式环境下&#xff0c;分布式锁可以保证在多个节点上的并发操作时数据的一致性和互斥性。分布式锁有多种实现方案&#xff0c;最常用的两种方案是&#xff1a;zookeeper和redis&#xff0c;本文介绍redis实现分布式锁方案…

【办公类-48-04】202506每月电子屏台账汇总成docx-5(问卷星下载5月范围内容,自动获取excel文件名,并转移处理)

背景需求&#xff1a; 1-4月电子屏表格&#xff0c;都是用这个代码将EXCEL数据整理成分类成3个WORD表格。 【办公类-48-04】20250118每月电子屏台账汇总成docx-4&#xff08;提取EXCLE里面1月份的内容&#xff0c;自制月份文件夹&#xff09;-CSDN博客文章浏览阅读1.2k次&…

【websocket】安装与使用

websocket安装与使用 1. 介绍2. 安装3. websocketpp常用接口4. Websocketpp使用4.1 服务端4.2 客户端 1. 介绍 WebSocket 是从 HTML5 开始支持的一种网页端和服务端保持长连接的 消息推送机制。 传统的 web 程序都是属于 “一问一答” 的形式&#xff0c;即客户端给服务器发送…

微算法科技(NASDAQ:MLGO)基于信任的集成共识和灰狼优化(GWO)算法,搭建高信任水平的区块链网络

随着数字化转型的加速&#xff0c;区块链技术作为去中心化、透明且不可篡改的数据存储与交换平台&#xff0c;正逐步渗透到金融、供应链管理、物联网等多个领域&#xff0c;探索基于信任的集成共识机制&#xff0c;并结合先进的优化算法来提升区块链网络的信任水平&#xff0c;…

【项目实战】通过多模态+LangGraph实现PPT生成助手

PPT自动生成系统 基于LangGraph的PPT自动生成系统&#xff0c;可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析&#xff1a;自动解析Markdown文档结构PPT模板分析&#xff1a;分析PPT模板的布局和风格智能布局决策&#xff1a;匹配内容与合适的PPT布局自动…

贝叶斯优化+LSTM+时序预测=Nature子刊!

贝叶斯优化与LSTM的融合在时间序列预测领域取得了显著成效&#xff0c;特别是在处理那些涉及众多超参数调整的复杂问题时。 1.这种结合不仅极大提高了预测的精确度&#xff0c;还优化了模型训练流程&#xff0c;提升了效率和成本效益。超参数优化的新篇章&#xff1a;LSTM因其…

AWSLambda之设置时区

目标 希望Lambda运行的时区是东八区。 解决 只需要设置lambda的环境变量TZ为东八区时区即可&#xff0c;即Asia/Shanghai。 参考 使用 Lambda 环境变量

RAG系统向量数据库选型与Prompt Engineering鲁棒性测试实践

引言 在AI应用不断落地的今天&#xff0c;RAG&#xff08;Retrieval-Augmented Generation&#xff0c;检索增强生成&#xff09;和Prompt Engineering&#xff08;提示工程&#xff09;成为大模型工程师和测试工程师的核心武器。 一方面&#xff0c;RAG系统依赖强大的向量数据…

2.Socket 编程 UDP

1.UDP网络编程 0.背景知识 自实现IP转化 相关函数理解 IP相关理解 1. V2版本 - DictServer封装版 实现一个简单的英译汉的网络字典 Dict.hpp dictionary.txt InetAddr.hpp • 在 InetAddr 中&#xff0c;重载一下方便对用户是否是同一个进行比较 Log.hpp makefile Mutex.hpp…

数据可视化交互

目录 【实验目的】 【实验原理】 【实验环境】 【实验步骤】 一、安装 pyecharts 二、下载数据 三、实验任务 实验 1&#xff1a;AQI 横向对比条形图 代码说明&#xff1a; 运行结果&#xff1a; 实验 2&#xff1a;AQI 等级分布饼图 实验 3&#xff1a;多城市 AQI…

【MATLAB去噪算法】基于CEEMDAN联合小波阈值去噪算法(第四期)

CEEMDAN联合小波阈值去噪算法相关文献 一、EMD 与 EEMD 的局限性 &#xff08;1&#xff09;EMD (经验模态分解) 旨在自适应地将非线性、非平稳信号分解成一系列 本征模态函数 (IMFs)&#xff0c;这些 IMFs 从高频到低频排列。 核心问题&#xff1a;模态混合 (Mode Mixing) 同…

大话软工笔记—架构模型

1. 架构模型1—拓扑图 &#xff08;1&#xff09;拓扑图概念 拓扑图&#xff0c;将多个软件系统用网络图连接起来的表达方式。 &#xff08;2&#xff09;拓扑图分类 总线型结构 比较普遍采用的方式&#xff0c;将所有的系统接到一条总线上。 星状结构 各个系统通过点到…

24-Oracle 23 ai ​Lock-Free Reservations​(无锁列值保留)

数据库领域为了解决ACID的平衡&#xff0c;尝试了各种锁、各种模式&#xff0c; 引擎技术特性、厂家实现方式各放异彩&#xff0c;被各种锁折磨的小伙伴&#xff0c;是不是感同身受。 一、数据库锁 1. 锁的类型与特点 ​全局锁​&#xff1a;锁定整个数据库实例&#xff0c;备…

OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别

OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding

从零写一个ALSA声卡驱动学习(1)

前言&#xff1a; 本文档描述了如何编写 ALSA&#xff08;高级 Linux 音频架构&#xff09;驱动程序。文档主要聚焦于 PCI 声卡的实现。对于其他类型的设备&#xff0c;可能会使用不同的 API。不过&#xff0c;至少 ALSA 的内核 API 是一致的&#xff0c;因此本文档在编写这些驱…

链结构与工作量证明7️⃣:用 Go 实现比特币的核心机制

链结构与工作量证明:用 Go 实现比特币的核心机制 如果你用 Go 写过区块、算过哈希,也大致理解了非对称加密、数据序列化这些“硬核知识”,那么恭喜你,现在我们终于可以把这些拼成一条完整的“区块链”。 不过别急,这一节我们重点搞懂两件事: 区块之间是怎么连接成“链”…

深入理解 React 样式方案

React 的样式方案较多,在应用开发初期,开发者需要根据项目业务具体情况选择对应样式方案。React 样式方案主要有: 1. 内联样式 2. module css 3. css in js 4. tailwind css 这些方案中,均有各自的优势和缺点。 1. 方案优劣势 1. 内联样式: 简单直观,适合动态样式和…

YOLO电力物目标检测训练

最近需要进行电力物检测相关的业务&#xff0c;因此制作了一个电力物数据集&#xff0c;使用YOLO目标检测方法进行实验&#xff0c;记录实验过程如下&#xff1a; 数据集标注 首先需要对电力物相关设备进行标注&#xff0c;这里我们选用labelme进行标注&#xff0c;使用无人机…

从阿里云域名解析异常事件看下域名解析过程

近日阿里云核心域名aliyuncs.com解析异常&#xff0c;涉及众多依赖阿里云服务的网站和应用&#xff0c;故障从发现到修复耗时5个多小时。本文简要分析下整个事件的过程&#xff0c;并分析域名解析的过程&#xff0c;了解根域名服务器在其中的作用&#xff0c;以了解。 1、aliyu…

应用分享 | 精准生成和时序控制!AWG在确定性三量子比特纠缠光子源中的应用

在量子技术飞速发展的今天&#xff0c;实现高效稳定的量子态操控是推动量子计算、量子通信等领域迈向实用化的关键。任意波形发生器&#xff08;AWG&#xff09;作为精准信号控制的核心设备&#xff0c;在量子实验中发挥着不可或缺的作用。丹麦哥本哈根大学的研究团队基于单个量…