********************父組件**********************
<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 })
}
實現效果圖