<script setup lang="tsx"> import { computed, ref, onMounted, reactive, nextTick, watch } from 'vue'; import { useAppStore } from '@/store/modules/app'; import { ArrowLeft20Filled } from '@vicons/fluent'; import { useMessage, NConfigProvider, darkTheme, NButton, NImage, NSpace, NTag, useNotification, NDropdown, NRow, NCol, NEllipsis } from "naive-ui" import HeaderBanner from './modules/header-banner.vue'; import CardData from './modules/card-data.vue'; import LineChart from './modules/line-chart.vue'; import RingChart from './modules/ring-chart.vue'; import PieChart from './modules/pie-chart.vue'; import ProjectNews from './modules/project-news.vue'; import CreativityBanner from './modules/creativity-banner.vue'; import { useRouter } from 'vue-router'; import { localStg } from '@/utils/storage'; // // api_logCount api_taskCount api_pointsCount import { api_logCount, api_modifyAlarmLog, api_getCameraList, api_getAreaNode, api_taskCount, api_pointsCount, api_getAlarmLog } from '@/api/index.ts' import JSMpeg from '@cycjimmy/jsmpeg-player'; import fullScreen from '@/utils/full' // import { wx } from '@/index.vue' // 服务器IP地址 // const serviceUrl = 'ws://192.168.1.140:9999'; // ws://192.168.1.221:9999 // const serviceUrl = 'ws://192.168.1.199:9999'; // const serviceUrl = 'ws://192.168.1.221:9999'; // const apiUrl = 'http://192.168.1.120:9995'; // const webSocketUrl = 'ws://192.168.1.120:9995'; const serviceUrl = import.meta.env.VITE_VIDEO_URL; const apiUrl = import.meta.env.VITE_SERVICE_URL; const webSocketUrl = import.meta.env.VITE_WEBSOCKET_URL; const router = useRouter(); const appStore = useAppStore(); const message = useMessage(); // notification const notification = useNotification(); const gap = computed(() => (appStore.isMobile ? 0 : 16)); const s = ref<[number, number]>([1183135260000, Date.now()]); let jsmpeg = ref(false); const tableData = ref([]); // 获取日志 const getAlarmLog = async () => { const param = { pageNum: 1, pageSize: 10 }; await api_getAlarmLog(param).then(res => { if (res.data.code === 200) { tableData.value = res.data.data.records; } }); }; const stop = () => { jsmpeg.value.pause(); }; const start = () => { jsmpeg.value.play(); } const cameraList = ref([]); // 获取区域 const getCameraList = async () => { const params = { pageNum: 1, pageSize: 10000, resourceNo: '100001000000000' }; await api_getCameraList(params).then(res => { if (res.data.code === 200) { cameraList.value = res.data.data.records; cameraList.value.length > 0 && switchVideoPalyer(cameraList.value[0].id); } }); }; const htmlStr = ref(''); const audioId = ref(''); const playAudio = async (id: string) => { /** * 1. 接口形式并非准确的资源形式形式 * 2. 模板的形式, 特殊处理 * 3. 动态js网络链接不上 */ htmlStr.value = `<audio ref="audioE" id="aWrap" controls> <source id="audio" src="${apiUrl}/v1/play/${id}" type="audio/mpeg"></source> </audio>`; nextTick(() => { let a = document.getElementById('aWrap'); a?.play(); }); }; const cId = ref(''); const cName = ref(''); const switchVideoPalyer = (id: string) => { cId.value = id; let camera = cameraList.value.filter(item => item?.id === id)[0]; console.log("======================================= id", id, camera?.cameraAddress); if (!camera?.cameraAddress?.includes('rtsp://')) { return message.error('摄像头流地址格式有误!'); } cName.value = camera.cameraName; jsmpeg.value && jsmpeg.value.destroy(); nextTick(() => { try { const rtsp1 = camera.cameraAddress; jsmpeg.value = new JSMpeg.Player( `${serviceUrl}/rtsp?url=${btoa(rtsp1)}&-s=1920x1080&fps=30`, { canvas: document.getElementById('canvas-4'), preserveDrawingBuffer: true, controls: true, videoBufferSize: 1024 * 1024 * 2 } ); } catch (e) { console.log("======================================= error", e); } }); }; const range = ref<[number, number]>([Date.now() - 86400000 * 30, Date.now()]); const options = [ { label: '10分钟内不在提醒!', key: '10', props: { onClick: () => { message.success('Good!') } } }, { label: '30分钟内不在提醒!', key: '30', props: { onClick: () => { message.success('Good!') } } }, { label: '关闭弹窗提醒', key: 'close', props: { onClick: () => { message.success('Good!') } } } ]; const handleSelect = (key: string) => { console.log(key); }; // 变更状态 const modifyAlramStatus = async (id: string, status: string) => { await api_modifyAlarmLog(id, status).then(res => { if (res.data.code === 200) { message.success(res.data.msg) } else { message.error(res.data.msg) } }) }; const showTipModal = ref(false); const logInfo = ref({ algorithmGrade: 0, algorithmName: 0, createTime: 0, id: 0, image: '', remark: '', reminderType: "", status: "", videoName: "" }); const toLogPage = (id: string) => { router.push({ path: `/alarmcenter/alarmlog?id=${id}`, }) } let showModal = ref(false); const isDefaultPreview = ref(false); let cLogInfo = reactive({}); // 切换视频播放源头 const switchVideoPlayer = () => { console.log("switchVideoPlayer ==============", "rtsp://admin:gemho10-7@192.168.0.56:554/h264/ch1/main/av_stream`") // nextTick(() => { // try { // // 临时测试 // const rtsp1 = 'rtsp://admin:gemho10-7@192.168.0.56:554/h264/ch1/main/av_stream'; // // eslint-disable-next-line no-new // new JSMpeg.Player( // `ws://192.168.1.147:9999/rtsp?url=${btoa(rtsp1)}&scale=640:-1&-b:v=1k&brightness=0.2&saturation=1.8`, // { // canvas: document.getElementById('canvas-3'), // preserveDrawingBuffer: true // } // ); // } catch (e) { } // }) } const devData = ref({}); const getCardData = async () => { await api_pointsCount().then(res => { if (res.data.code === 200) { devData.value = res.data.data; }; }) }; const openCamera = (item) => { // e.stopPropagation(); // e.preventDefault(); isDefaultPreview.value = true; switchVideoPlayer(); cLogInfo = item; showModal.value = true; } const full = ref(false); // watch(full, (oldType, curType) => { // if (curType) { // nextTick(() => { // const el = document.getElementById('canvas-4'); // const { clientWidth, clientHeight } = el; // console.log(clientWidth, clientHeight) // el.width = clientWidth; // el.height = clientHeight; // switchVideoPalyer(); // }) // } // }); /** * 窗口自适应 */ // window.addEventListener('resize', (e) => { // if (full.value) { // } // }) onMounted(() => { getAlarmLog(); getCameraList(); getCardData(); // =============================================================== Demo | S try { let ws = new WebSocket(`${webSocketUrl}/ws/${localStg.get('id')}`); ws.onopen = function () { console.log('WebSocket connected --------------------------------------------------------------------------'); ws.send('Hello, Server! ------------------------- '); }; ws.onmessage = function (event) { console.log('---------------------- WebSocket received message:', JSON.parse(event.data)); let data = JSON.parse(event.data); data.algorithmId && playAudio(data.algorithmId); if (data.reminderType === 1) { window.$notification?.create({ duration: 5000, title: () => <div> <NSpace justify='space-between'> <NSpace align='center' > { data.algorithmGrade === 0 ? ( <NTag type="error">一级</NTag> ) : data.algorithmGrade === 1 ? ( <NTag type="warning">二级</NTag> ) : data.algorithmGrade === 2 ? ( <NTag type="info">三级</NTag> ) : data.algorithmGrade === 3 ? ( <NTag type="success">四级</NTag> ) : ( <NTag>五级</NTag> ) } {data.algorithmName} {data.id} </NSpace> {/* <NDropdown trigger="hover" options={options} onSelect={handleSelect}> <NButton secondary size="small" text >暂停提醒</NButton> </NDropdown> */} </NSpace> {/* 提醒 ================================= E */} <NRow gutter={10} style={{ marginTop: '10px' }}> <NCol span="12"> <img style={{ height: "100px", width: '200px' }} src={data.image} /> </NCol> <NCol span="12"> {/* <div> */} <n-space vertical justify="space-between" > <div> <n-button quaternary round> {data.cameraName} </n-button> <div style={{ fontSize: '14px' }} > <n-button quaternary round type="info" onClick={toLogPage}> 查看详情 </n-button> </div> </div> <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: "20px" }}> <NButton strong secondary size="small" onClick={() => modifyAlramStatus(data.id, '1')}>正确报警</NButton> <NButton strong secondary style={{ marginLeft: '20px' }} size="small" onClick={() => modifyAlramStatus(data.id, '2')} >错误报警</NButton> </div> {/* </div> */} </n-space> </NCol> </NRow> </div> }); } else { logInfo.value = data; showTipModal.value = true; } }; ws.onclose = function () { console.log('WebSocket closed'); }; ws.onerror = function (error) { console.error('WebSocket error:', error); }; } catch (e) { console.log("error", e) } /** * * 默认全屏后, 游览器默认行为, 优先征用 keyup / keydown , 全屏后, 第一次 esc 不会被触发,存在优先级。 * 1. fullscreenchange 事件, hack 处理 */ document.addEventListener('fullscreenchange', (e) => { if (!document.fullscreenElement) { full.value = false } }); }); </script> <template> <div id="HomePage"> <!-- <canvas height="1080" width="1920" id="canvas-4"></canvas> --> <!-- =================================== 弹窗 | S --> <NModal v-model:show="showModal"> <NCard v-if="isDefaultPreview" style="width: 800px;" @close="showModal = false" closable> <template #header> <NFlex justify=" space-between"> <NFlex justify="start" align="center"> <NTag size="small" v-if="cLogInfo.algorithmGrade === 0" type="error"> 一 级 </NTag> <NTag size="small" v-else-if="cLogInfo.algorithmGrade === 1" type="warning"> 二 级 </NTag> <NTag size="small" v-else-if="cLogInfo.algorithmGrade === 2" type="info"> 三 级 </NTag> <NTag size="small" v-else-if="cLogInfo.algorithmGrade === 3" type="success"> 四 级 </NTag> <NTag size="small" v-else>五级</NTag> <NSpace style="font-size: 14px;"> <span>{{ cLogInfo.videoName }}</span> <span>{{ cLogInfo.cameraName }}</span> <span>{{ cLogInfo.createTime }}</span> </NSpace> </NFlex> <NButton @click.stop="() => { isDefaultPreview = false; }" strong type="info" size="small"> <template #icon> <icon-ic:baseline-slow-motion-video class="text-icon" /> </template> 回放 </NButton> </NFlex> </template> <!-- 图片区域 --> <div style="width: 100%; height: 400px; box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);"> <NImage :src="cLogInfo?.image" /> </div> <!-- 操作区域 --> <template #footer> <NFlex justify="center"> <NSpace> <n-button size="small" quaternary round type="primary" @click.stop="() => modifyAlramStatus(cLogInfo?.id, '1')">正确报警</n-button> <n-button size="small" quaternary round type="error" @click.stop="() => modifyAlramStatus(cLogInfo?.id, '2')">错误报警</n-button> </NSpace> </NFlex> </template> </NCard> <NCard v-else style="width: 800px;" @close="showModal = false" closable> <template #header> <span @click="() => { switchVideoPlayer(); isDefaultPreview = true; } "> <NIcon size="25" style="position: relative; top: 2px;"> <ArrowLeft20Filled /> </NIcon> <span style="position: relative; top: -1px; margin-left: 5px"></span> </span> <NDivider vertical /> {{ cLogInfo.cameraName }} </template> <!-- 视频回放 --> <div style="width: 100%; height: 400px; padding-bottom: 10px;"> <!-- MP4 切换视频 --> <video controls style="width: 100%; height: 100%; box-shadow: 0 0 10px rgba(255, 255, 255, 0.1);"> <source :src="`${apiUrl}/v1/playVideo/${cLogInfo.remark}`" type="audio/mpeg"> </source> </video> </div> </NCard> </NModal> <!-- =================================== 弹窗 | E --> <div style="border: 1px solid red; position: fixed; top: -1000px" v-html="htmlStr"></div> <NSpace vertical :size="16"> <NModal v-model:show="showTipModal"> <NCard style="width: 800px" :bordered="false" @close="showTipModal = false" closable> <template #header> <NSpace align="center"> <NTag size="large" v-if="logInfo.algorithmGrade === 0" type="error"> 一 级 </NTag> <NTag size="large" v-else-if="logInfo.algorithmGrade === 1" type="warning"> 二 级 </NTag> <NTag size="large" v-else-if="logInfo.algorithmGrade === 2" type="info"> 三 级 </NTag> <NTag size="large" v-else-if="logInfo.algorithmGrade === 3" type="success"> 四 级 </NTag> <NTag size="large" v-else>五级</NTag> {{ logInfo?.algorithmName || '张三' }} <!-- 视频文件 --> <n-button quaternary round> {{ logInfo?.videoName }} </n-button> <!-- 查看详情 --> <n-button quaternary round @click="toLogPage"> 查看详情 </n-button> </NSpace> </template> <div style="width: 100%; height: 400px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);"> <NImage :src="logInfo?.image" /> </div> <template #footer> <NFlex justify="center"> <NSpace> <n-button quaternary round type="primary" @click.stop="() => modifyAlramStatus(logInfo?.id, '1')">正确报警</n-button> <n-button quaternary round type="error" @click.stop="() => modifyAlramStatus(logInfo?.id, '2')">错误报警</n-button> </NSpace> </NFlex> </template> </NCard> </NModal> <!-- <HeaderBanner /> --> <!-- ========================= 实时监控 | S --> <NRow :gutter="15"> <NCol :span="18" class="text-focus-in"> <NRow :gutter="15"> <NCol :span="6"> <NCard> <template #header> 点位状态 </template> <!-- <CardData /> --> <div style="height: 100px; display: flex; justify-content: space-between; align-items: center;"> <div style="flex: 1; text-align: center;"> <div style="display: inline-block; font-size: 40px; font-weight: 500;"> {{ devData[0]?.count || 0 }} </div> <br /> <div style="display: inline-block;margin-top: 10px; margin-bottom: 20px;"> <img src="../../assets/z_03.jpg" alt=""> </div> </div> <div style="flex: 1; text-align: center;"> <div style="display: inline-block;font-size: 40px; font-weight: 500;"> {{ devData[1]?.count || 0 }} </div> <br /> <div style="display: inline-block; margin-top: 10px; margin-bottom: 20px;"> <img src="../../assets/z_05.jpg" alt=""> </div> </div> </div> </NCard> <!-- 回放 --> <NCard style="margin-top: 10px; height: 300px"> <template #header> 视频分析任务启动状态 </template> <RingChart></RingChart> </NCard> </NCol> <NCol :span="18" style="height: 500px"> <NCard :bordered="false"> <!-- 标题 --> <template #header> <span v-if="cName">{{ `${cName}` }}<span style="margin-left: 10px; font-size: 14px;">正在查看</span></span> <span v-else>{{ `实时监控` }}</span> </template> <template #header-extra> <div style="width: 200px;"> <NSelect @update:value="val => switchVideoPalyer(val)" placeholder="请选择" label-field="cameraName" value-field="id" :options="cameraList"></NSelect> </div> </template> <!-- 实时监控 | S --> <!-- height: 450px; --> <!-- /** .video-player { border: 1px solid #ccc; padding: 2px; border: #ccc; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 99999; }; /* ============================= 推理平台 | S */ .video-player-old { padding: 2px; border: #ccc; height: 420px !important; /* height: 100% !important; */ border: 1px solid #ccc; border: 10px solid red !important; }; */ --> <!-- :class="[full ? 'video-player' : 'video-player-old']" --> <div :style="full ? { border: '1px solid #ccc', padding: '2px', position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', zIndex: '99999' } : { padding: '2px', height: '410px !important', border: ' 1px solid #ccc', position: 'relative' }"> <div style="position: absolute; bottom: 20px; right: 30px; width: 40px; height: 40px; cursor: pointer; "> <icon-gridicons-fullscreen-exit v-if="full" color="#1872F0" font-size="30" @click="full = fullScreen(full)" /> <icon-gridicons-fullscreen v-else font-size="30" color="#1872F0" @click="full = fullScreen(full)" /> </div> <canvas style="width: 100%; height: 100%;" width="800" height="600" id="canvas-4"></canvas> </div> </NCard> </NCol> </NRow> <NGrid style="padding-top: 20px" :x-gap="gap" :y-gap="16" responsive="screen" item-responsiv> <NGi span="24 s:24 m:24"> <NCard :bordered="false" title="统计分析 (条/天)" class="card-wrapper"> <template #header-extra> <n-date-picker v-model:value="range" type="daterange" clearable /> </template> <div style="height: 400px"> <LineChart :date="range" /> </div> </NCard> </NGi> </NGrid> </NCol> <NCol :span="6"> <NCard class="text-focus-in" :bordered="false" title="实时报警" :padding="0"> <n-infinite-scroll class="aralm-scroll" :style="{ height: '970px', padding: '0 10px', '--n-scrollbar-color': 'transparent' }" :distance="10"> <NRow v-for="item in tableData" :gutter="20" style="margin-top: 10px; border-radius: 7px; overflow: hidden; padding: 10px 10px; position: relative; display: flex; justify-content: center; cursor: pointer; align-items: center;"> <NCol :span="12"> <n-space justify="space-around" size="large" vertical @click="(e) => { e.stopPropagation(); e.preventDefault(); openCamera(item); }"> <div> <NSpace> <NTag v-if="item.algorithmGrade === 0" type="error"> 一 级 </NTag> <NTag v-else-if="item.algorithmGrade === 1" type="warning"> 二 级 </NTag> <NTag v-else-if="item.algorithmGrade === 2" type="info"> 三 级 </NTag> <NTag v-else-if="item.algorithmGrade === 3" type="success"> 四 级 </NTag> <NTag v-else>五级</NTag> <h1> {{ item.algorithmName }}</h1> </NSpace> </div> <div>{{ item.cameraName }}</div> <div>{{ item.createTime }}</div> </n-space> </NCol> <!-- image --> <NCol :span="12"> <img style="height: 100px; width: 100%; box-shadow: 0 0 10px rgba(0,0,0,.3);" :src="item.image"> </img> </NCol> </NRow> </n-infinite-scroll> </NCard> </NCol> </NRow> <!-- ========================= 实时监控 | E --> </NSpace> </div> </template> <style scoped lang="scss"> /** 报警 - 滚动条 */ #HomePage::global(.n-scrollbar) { --n-scrollbar-color: transparent !important; } .text-focus-in { /* http://192.168.1.171:9527/demo: text-focus-in 1s cubic-bezier(0.550, 0.085, 0.680, 0.530) both; */ } /* ---------------------------------------------- * Generated by Animista on 2024-10-29 15:10:15 * Licensed under FreeBSD License. * See http://animista.net/license for more info. * w: http://animista.net, t: @cssanimista * ---------------------------------------------- */ /** * ---------------------------------------- * animation text-focus-in * ---------------------------------------- */ @-webkit-keyframes text-focus-in { 0% { -webkit-filter: blur(12px); filter: blur(12px); opacity: 0; } 100% { -webkit-filter: blur(0px); filter: blur(0px); opacity: 1; } } @keyframes text-focus-in { 0% { -webkit-filter: blur(12px); filter: blur(12px); opacity: 0; } 100% { -webkit-filter: blur(0px); filter: blur(0px); opacity: 1; } } #canvas-4 { width: 100%; height: calc(100% - 20px); } .blinking-red { box-shadow: inset 0 0 10px rgba(239, 108, 108, .9); animation: blink 3s linear infinite; border-radius: 7px !important; overflow: hidden !important; cursor: pointer; } @keyframes blink { 0% { box-shadow: inset 0 0 30px rgba(239, 108, 108, .9); } 25% { box-shadow: inset 0 0 50px rgba(239, 108, 108, .9); border: 1px solid rgba(239, 108, 108, .9); } 50% { /* opacity: 0; */ box-shadow: inset 0 0 10px rgba(239, 108, 108, .9); translate: scaale(1.05) } 75% { box-shadow: inset 0 0 30px rgba(239, 108, 108, .9); } 100% { box-shadow: inset 0 0 50px rgba(239, 108, 108, .9); border: 1px solid rgba(239, 108, 108, .9); } } blinking-red::after { content: " 张三"; /* display: ; */ } .video-player { border: 1px solid #ccc; padding: 2px; border: #ccc; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 99999; } ; /* ============================= 推理平台 | S */ .video-player-old { padding: 2px; border: #ccc; height: 420px !important; /* height: 100% !important; */ border: 1px solid #ccc; border: 10px solid red !important; } ; </style>