feat:视频回放
This commit is contained in:
1 parent
e0f2147882
commit
f18796aef3
14 files changed
+487
-8
No files matched your search
@@ -1 +1 @@
|
||||
var __defProp=Object.defineProperty,__defNormalProp=(e,t,s)=>t in e?__defProp(e,t,{enumerable:!0,configurable:!0,writable:!0,value:s}):e[t]=s,__publicField=(e,t,s)=>(__defNormalProp(e,"symbol"!=typeof t?t+"":t,s),s);!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).WSPlayer={})}(this,(function(e){"use strict";const t={websocketPorts:{realmonitor:"realmonitor-websocket",playback:"playback-websocket",realmonitor_ws:"9100",playback_ws:"9320",realmonitor_wss:"9102",playback_wss:"9322"},errorVideoInfo:{100:"浏览器资源不足",101:"当前浏览器不支持硬解码",102:"硬解码播放失败",103:"硬解码播放延时超过8秒",104:"硬解码失败",201:"当前音频无法播放",202:"websocket连接错误",203:"文件播放完成",401:"该用户无操作权限",404:"请求超时或未找到",405:"播放超时",406:"视频流类型解析失败,请检查通道配置",407:"请求超时",408:"请求超时或码流不支持",409:"服务未推送码流",410:"视频流异常断开",411:"录像播放完成",457:"时间设置错误",503:"WS连接地址错误,非当前连接的ICC服务器返回",504:"对讲失败",701:"Chrome版本低,请升级到最新的Chrome版本",702:"Firefox版本低,请升级到最新的Firefox版本",703:"Edge版本低,请升级到最新的Edge版本",defaultErrorMsg:"播放失败,请检查配置"},errorInfo:{101:"所选通道离线,无法播放",102:"登录权限过期,查询实时预览rtsp失败",103:"获取实时预览rtsp失败",104:"操作失败,请稍后重试",105:"通道休眠,正在唤醒中,请稍后再试",106:"请传入rtsp地址和websocket地址",107:"当前未传records录像文件, 将无法拖动进度条",108:"请求超时,请稍后重试",201:"所选通道未查询到录像文件",202:"查询录像文件列表失败",203:"查询录像rtsp失败",204:"{0}倍速无法播放音频",205:"通道休眠,正在唤醒中,请稍后再试",206:"当前倍速不支持",301:"正在对讲,无法关闭音频",302:"其他设备对讲中,无法开启音频",303:"其他设备对讲中,无法开启对讲",304:"查询对讲rtsp失败",305:"http协议不支持对讲",306:"设备对讲失败",307:"不支持PCM音频格式对讲",308:"不支持ARM音频格式对讲",309:"不支持G711u音频格式对讲",310:"不支持G726音频格式对讲",311:"不支持AAC音频格式对讲",312:"不支持G722音频格式对讲",402:"该用户无播放权限",501:"解码库未初始化完成,请稍后播放!",502:"解码库未初始化完成,请稍后对讲!",503:"请检查创建播放器时,播放器容器是否存在",601:"所操作播放器不存在",602:"所选播放器正在本地录像中,不可重复本地录像",603:"所选播放器未播放录像,不可本地录像",604:"所选播放器未开始本地录像,不可操作关闭本地录像",605:"时间跳转播放传参错误",606:"设置自适应拉伸传参错误",607:"实时预览不支持倍速播放",608:"需传入正确的{0}方法:{1}",609:"超出当前设置最大窗口布局数量",610:"操作自定义窗口布局失败:{0}",611:"实时预览不支持播放方法",612:"实时预览不支持暂停方法",613:"实时预览不支持跳转播放",614:"当前窗口未播放实时预览画面, 无法进行对讲",701:"云台被用户{0}锁定,无法操作",702:"控制云台三维定位失败{0}",703:"控制云台{0}{1}失败{2}",704:"控制云台方向失败{0}",30000001:"SDP初始化失败",30000002:"接入服务内部错误(信令建立过程)",30000101:"请求失败",30000102:"拉流过程异常中断",30000103:"接入服务内部异常(码流传输过程)",30000104:"老设备回放时登入设备失败",30000105:"设备离线",30000106:"通道离线",30000107:"设备被删除",30000108:"通道被删除",30001001:"创建子链接失败",30001002:"设备不支持",30001003:"设备资源不足",30001004:"查询不到录像",30001005:"设备返回超时",30001006:"超过最大连接数",30002001:"拒绝对讲",30002002:"对讲被其他客户端打开,请稍后重试",30002003:"资源冲突",30002004:"不支持的语音编码格式",30002005:"无权限",30002006:"请求对讲失败",30002007:"请求喊话失败",30003001:"码率超限"},windowDefaultCustomDivision:{2:[{lStep:0,tStep:0,wStep:50,hStep:100},{lStep:50,tStep:0,wStep:50,hStep:100}],3:[{wStep:50,hStep:100,tStep:0,lStep:0},{wStep:50,hStep:50,tStep:0,lStep:50},{wStep:50,hStep:50,tStep:50,lStep:50}],6:[{wStep:66,hStep:66,tStep:0,lStep:0,selectIndex:0},{wStep:34,hStep:33,tStep:0,lStep:66,selectIndex:1},{wStep:34,hStep:33,tStep:33,lStep:66,selectIndex:2},{wStep:33,hStep:34,tStep:66,lStep:0,selectIndex:3},{wStep:33,hStep:34,tStep:66,lStep:33,selectIndex:4},{wStep:34,hStep:34,tStep:66,lStep:66,selectIndex:5}],8:[{wStep:75,hStep:75,tStep:0,lStep:0,selectIndex:0},{wStep:25,hStep:25,tStep:Line truncated
|
||||
var __defProp = Object.defineProperty, __defNormalProp = (e, t, s) => t in e ? __defProp(e, t, { enumerable: !0, configurable: !0, writable: !0, value: s }) : e[t] = s, __publicField = (e, t, s) => (__defNormalProp(e, "symbol" != typeof t ? t + "" : t, s), s); !function (e, t) { "object" == typeof exports && "undefined" != typeof module ? t(exports) : "function" == typeof define && define.amd ? define(["exports"], t) : t((e = "undefined" != typeof globalThis ? globalThis : e || self).WSPlayer = {}) }(this, (function (e) { "use strict"; const t = { websocketPorts: { realmonitor: "realmonitor-websocket", playback: "playback-websocket", realmonitor_ws: "9100", playback_ws: "9320", realmonitor_wss: "9102", playback_wss: "9322" }, errorVideoInfo: { 100: "浏览器资源不足", 101: "当前浏览器不支持硬解码", 102: "硬解码播放失败", 103: "硬解码播放延时超过8秒", 104: "硬解码失败", 201: "当前音频无法播放", 202: "websocket连接错误", 203: "文件播放完成", 401: "该用户无操作权限", 404: "请求超时或未找到", 405: "播放超时", 406: "视频流类型解析失败,请检查通道配置", 407: "请求超时", 408: "请求超时或码流不支持", 409: "服务未推送码流", 410: "视频流异常断开", 411: "录像播放完成", 457: "时间设置错误", 503: "WS连接地址错误,非当前连接的ICC服务器返回", 504: "对讲失败", 701: "Chrome版本低,请升级到最新的Chrome版本", 702: "Firefox版本低,请升级到最新的Firefox版本", 703: "Edge版本低,请升级到最新的Edge版本", defaultErrorMsg: "播放失败,请检查配置" }, errorInfo: { 101: "所选通道离线,无法播放", 102: "登录权限过期,查询实时预览rtsp失败", 103: "获取实时预览rtsp失败", 104: "操作失败,请稍后重试", 105: "通道休眠,正在唤醒中,请稍后再试", 106: "请传入rtsp地址和websocket地址", 107: "当前未传records录像文件, 将无法拖动进度条", 108: "请求超时,请稍后重试", 201: "所选通道未查询到录像文件", 202: "查询录像文件列表失败", 203: "查询录像rtsp失败", 204: "{0}倍速无法播放音频", 205: "通道休眠,正在唤醒中,请稍后再试", 206: "当前倍速不支持", 301: "正在对讲,无法关闭音频", 302: "其他设备对讲中,无法开启音频", 303: "其他设备对讲中,无法开启对讲", 304: "查询对讲rtsp失败", 305: "http协议不支持对讲", 306: "设备对讲失败", 307: "不支持PCM音频格式对讲", 308: "不支持ARM音频格式对讲", 309: "不支持G711u音频格式对讲", 310: "不支持G726音频格式对讲", 311: "不支持AAC音频格式对讲", 312: "不支持G722音频格式对讲", 402: "该用户无播放权限", 501: "解码库未初始化完成,请稍后播放!", 502: "解码库未初始化完成,请稍后对讲!", 503: "请检查创建播放器时,播放器容器是否存在", 601: "所操作播放器不存在", 602: "所选播放器正在本地录像中,不可重复本地录像", 603: "所选播放器未播放录像,不可本地录像", 604: "所选播放器未开始本地录像,不可操作关闭本地录像", 605: "时间跳转播放传参错误", 606: "设置自适应拉伸传参错误", 607: "实时预览不支持倍速播放", 608: "需传入正确的{0}方法:{1}", 609: "超出当前设置最大窗口布局数量", 610: "操作自定义窗口布局失败:{0}", 611: "实时预览不支持播放方法", 612: "实时预览不支持暂停方法", 613: "实时预览不支持跳转播放", 614: "当前窗口未播放实时预览画面, 无法进行对讲", 701: "云台被用户{0}锁定,无法操作", 702: "控制云台三维定位失败{0}", 703: "控制云台{0}{1}失败{2}", 704: "控制云台方向失败{0}", 30000001: "SDP初始化失败", 30000002: "接入服务内部错误(信令建立过程)", 30000101: "请求失败", 30000102: "拉流过程异常中断", 30000103: "接入服务内部异常(码流传输过程)", 30000104: "老设备回放时登入设备失败", 30000105: "设备离线", 30000106: "通道离线", 30000107: "设备被删除", 30000108: "通道被删除", 30001001: "创建子链接失败", 30001002: "设备不支持", 30001003: "设备资源不足", 30001004: "查询不到录像", 30001005: "设备返回超时", 30001006: "超过最大连接数", 30002001: "拒绝对讲", 30002002: "对讲被其他客户端打开,请稍后重试", 30002003: "资源冲突", 30002004: "不支持的语音编码格式", 30002005: "无权限", 30002006: "请求对讲失败", 30002007: "请求喊话失败", 30003001: "码率超限" }, windowDefaultCustomDivision: { 2: [{ lStep: 0, tStep: 0, wStep: 50, hStep: 100 }, { lStep: 50, tStep: 0, wStep: 50, hStep: 100 }], 3: [{ wStep: 50, hStep: 100, tStep: 0, lStep: 0 }, { wStep: 50, hStep: 50, tStep: 0, lStep: 50 }, { wStep: 50, hStep: 50, tStep: 50, lStep: 50 }], 6: [{ wStep: 66, hStep: 66, tStep:Line truncated
|
||||
@@ -8,6 +8,14 @@ export function list(query: any) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getReplayList(data: any) {
|
||||
return request({
|
||||
url: '/admin/deviceChannel/getReplayList',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchList(query?: Object) {
|
||||
return request({
|
||||
url: '/admin/deviceChannel/page',
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import request from "/@/utils/request"
|
||||
|
||||
/**
|
||||
* 视频回放列表
|
||||
*/
|
||||
export function defaultReplayVideoList(data: any) {
|
||||
return request({
|
||||
url: '/admin/third/camera/defaultReplayVideoList',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 报警录像信息列表
|
||||
*/
|
||||
export function alarmReplayVideoList(data: any) {
|
||||
return request({
|
||||
url: '/admin/third/camera/alarmReplayVideoList',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 报警录像信息列表
|
||||
*/
|
||||
export function getRtspReplayVideo(data: any) {
|
||||
return request({
|
||||
url: '/admin/third/camera/getRtspReplayVideo',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
@@ -79,6 +79,7 @@
|
||||
|
||||
<script setup lang="ts" name="DraggableOrgTree">
|
||||
import { computed } from "vue"
|
||||
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
import { getTree } from "/@/api/sys/sysOrg"
|
||||
|
||||
@@ -239,7 +240,11 @@ defineExpose({
|
||||
|
||||
<style>
|
||||
.splitpanes {
|
||||
height: initial !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.splitpanes>.splitpanes__pane {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.splitpanes--vertical>.splitpanes__splitter {
|
||||
|
||||
+2
-2
@@ -202,13 +202,13 @@ export function useTable(options?: BasicTableProps) {
|
||||
* 刷新后不跳转第一页,则入参 getDataList(false)
|
||||
* @param refresh 是否刷新当前页码
|
||||
*/
|
||||
const getDataList = (refresh?: any) => {
|
||||
const getDataList = async (refresh?: any) => {
|
||||
// 如果需要刷新,则将state.pagination.current重置为1
|
||||
if (refresh !== false) {
|
||||
state.pagination!.current = 1;
|
||||
}
|
||||
// 再次发起查询操作
|
||||
query();
|
||||
await query();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -182,6 +182,72 @@ export function parseTime(time, pattern) {
|
||||
return time_str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将日期数组转换为包含具体时间的格式
|
||||
* @param dateList 日期数组,格式如 ["2025-07-19", "2025-07-21"]
|
||||
* @param format 输出格式,默认为 'YYYY-MM-DD HH:mm:ss'
|
||||
* @returns 返回包含开始时间和结束时间的对象
|
||||
* @description 开始日期设置为 00:00:00,结束日期设置为 23:59:59
|
||||
*/
|
||||
export function formatDateRange(dateList: string[], format: string = 'YYYY-MM-DD HH:mm:ss'): {
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
startTimestamp: number;
|
||||
endTimestamp: number;
|
||||
} {
|
||||
if (!dateList || dateList.length !== 2) {
|
||||
throw new Error('dateList must be an array with exactly 2 elements');
|
||||
}
|
||||
|
||||
const [startDate, endDate] = dateList;
|
||||
|
||||
// 创建开始时间 (00:00:00)
|
||||
const startDateTime = new Date(`${startDate} 00:00:00`);
|
||||
// 创建结束时间 (23:59:59)
|
||||
const endDateTime = new Date(`${endDate} 23:59:59`);
|
||||
|
||||
// 格式化时间字符串
|
||||
const startTime = formatDate(startDateTime, format);
|
||||
const endTime = formatDate(endDateTime, format);
|
||||
|
||||
// 获取时间戳(秒)
|
||||
const startTimestamp = Math.floor(startDateTime.getTime() / 1000);
|
||||
const endTimestamp = Math.floor(endDateTime.getTime() / 1000);
|
||||
|
||||
return {
|
||||
startTime,
|
||||
endTime,
|
||||
startTimestamp,
|
||||
endTimestamp
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速转换日期数组为时间戳范围
|
||||
* @param dateList 日期数组,格式如 ["2025-07-19", "2025-07-21"]
|
||||
* @returns 返回开始和结束的时间戳(秒)
|
||||
*/
|
||||
export function getDateRangeTimestamps(dateList: string[]): {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
} {
|
||||
if (!dateList || dateList.length !== 2) {
|
||||
throw new Error('dateList must be an array with exactly 2 elements');
|
||||
}
|
||||
|
||||
const [startDate, endDate] = dateList;
|
||||
|
||||
// 创建开始时间 (00:00:00)
|
||||
const startDateTime = new Date(`${startDate} 00:00:00`);
|
||||
// 创建结束时间 (23:59:59)
|
||||
const endDateTime = new Date(`${endDate} 23:59:59`);
|
||||
|
||||
return {
|
||||
startTime: Math.floor(startDateTime.getTime() / 1000),
|
||||
endTime: Math.floor(endDateTime.getTime() / 1000)
|
||||
};
|
||||
}
|
||||
|
||||
// 日期格式化
|
||||
export function parseDate(time, pattern) {
|
||||
if (arguments.length === 0 || !time) {
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
<el-form-item style="width: 250px;" label="车主姓名" prop="personName">
|
||||
<el-input v-model="state.queryForm.personName" placeholder="请输入车主姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px;" label="车辆类型" prop="carType">
|
||||
<dict-select v-model="state.queryForm.carType" :options="dh_car_type" placeholder="请选择车辆类型" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="getDataList">
|
||||
{{ $t('common.queryBtn') }}
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -34,7 +34,7 @@ import * as DeviceChannelApi from "/@/api/device/deviceChannel"
|
||||
import * as DeviceCameraApi from "/@/api/device/deviceCamera"
|
||||
|
||||
// @ts-ignore
|
||||
import PlayerManager from "./lib/icc/PlayerManager";
|
||||
import PlayerManager from "../lib/icc/PlayerManager";
|
||||
|
||||
let realPlayer: any = null;
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div class="w-full h-full bg-emerald-50">
|
||||
<div id="ws-record-player" class="w-full h-full" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="ReplayVideo">
|
||||
import * as AlarmReplayVideoListApi from "/@/api/third/deviceCameraReplay"
|
||||
import { formatDateRange, getDateRangeTimestamps } from "/@/utils/formatTime"
|
||||
|
||||
const emit = defineEmits(['mounted'])
|
||||
|
||||
// @ts-ignore
|
||||
import PlayerManager from "../lib/icc/PlayerManager";
|
||||
|
||||
let replayVideo: any = null
|
||||
|
||||
const state = reactive<any>({
|
||||
startTimestamp: null,
|
||||
endTimestamp: null,
|
||||
queryForm: {},
|
||||
dialogVisible: false,
|
||||
channelList: [],
|
||||
loading: false,
|
||||
treeLoading: false,
|
||||
currentNum: 4,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
emit("mounted")
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
destroy()
|
||||
})
|
||||
|
||||
// 初始化播放器
|
||||
const init = (dataList: any, queryForm: any) => {
|
||||
state.queryForm = queryForm
|
||||
if (dataList && dataList.length) {
|
||||
state.channelList = dataList
|
||||
}
|
||||
|
||||
// 处理日期范围
|
||||
if (queryForm.videoDateList && queryForm.videoDateList.length == 2) {
|
||||
const dateRange = formatDateRange(queryForm.videoDateList)
|
||||
state.startTimestamp = dateRange.startTimestamp
|
||||
state.endTimestamp = dateRange.endTimestamp
|
||||
}
|
||||
|
||||
replayVideo = new PlayerManager({
|
||||
el: "ws-record-player", /** 实时预览容器id,创建多个播放器,传入不同的容器id即可 **/
|
||||
type: 'record', /** real - 实时预览 record - 录像回放 **/
|
||||
maxNum: 16, /** 一个播放器上限能播放的路数,可根据实际情况设置,支持 1 4 9 16 25 **/
|
||||
num: state.currentNum, /** 初始化,页面显示的路数 **/
|
||||
showControl: true, /** 是否显示工具栏,默认显示 **/
|
||||
prefixUrl: 'lib', /** 解码库所在位置的前缀 **/
|
||||
isRTPStream: true, /** true-标准码流拉流 false-私有码流拉流 **/
|
||||
receiveMessageFromWSPlayer: (methods: any, data: any, err: any) => { /* 回调函数,可以在以下回调函数里面做监听 */
|
||||
switch (methods) {
|
||||
case 'initializationCompleted':
|
||||
// 初始化完成,可以调用拉流方法,实现播放器加载后立即显示画面
|
||||
playRecord()
|
||||
break;
|
||||
case "recordSuccess":
|
||||
// 录像回放成功
|
||||
console.log("录像回放成功")
|
||||
break;
|
||||
case "recordError":
|
||||
// 录像回放失败
|
||||
console.log("录像回放失败", err)
|
||||
break;
|
||||
case "recordFinish":
|
||||
// 录像回放完成
|
||||
console.log("录像回放完成");
|
||||
break;
|
||||
// 拖拽时间戳的回调
|
||||
case 'switchStartTime':
|
||||
console.log("拖拽时间戳的回调:", data);
|
||||
|
||||
// data.timeStamp 时间戳
|
||||
// data.channelData 通道信息
|
||||
// {timeStamp, channelData: player.options.channelData, hasVideoRecord: true}
|
||||
// palyerManager.recordByUrl(recordPlayer.recordByUrl({
|
||||
// wsURL: '', // ws地址
|
||||
// rtspURL: '', // 重新请求后的rtsp
|
||||
// channelId , // 必传, 通道id
|
||||
// records: records, // 录像列表
|
||||
// selectIndex: 0, // 必传
|
||||
// playRecordByTime: false // 必传false
|
||||
// }))
|
||||
break;
|
||||
case 'selectWindowChanged':
|
||||
// 选中的窗口发生改变
|
||||
console.log(data, "返回窗口信息")
|
||||
break;
|
||||
case 'windowNumChanged':
|
||||
// 播放器显示的路数发生改变
|
||||
console.log(data, "返回显示的窗口数量")
|
||||
if (state.currentNum == data) return
|
||||
state.currentNum = data
|
||||
if (replayVideo) {
|
||||
for (let index = 0; index < state.currentNum; index++) {
|
||||
replayVideo.close(index)
|
||||
}
|
||||
}
|
||||
playRecord()
|
||||
break;
|
||||
case 'closeVideo': // 视频关闭回调
|
||||
// 点击关闭按钮引发的视频关闭进行提示
|
||||
// 切换视频引发的视频关闭不进行提示
|
||||
if (!data.changeVideoFlag) {
|
||||
console.log(`窗口${data.selectIndex}的视频已关闭`)
|
||||
}
|
||||
break;
|
||||
case 'statusChanged':
|
||||
// 视频状态发生改变
|
||||
break;
|
||||
case 'errorInfo':
|
||||
// 错误信息汇总
|
||||
console.log(data, "可打印查看错误消息");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 流地址方式播放录像回放
|
||||
const playRecord = async () => {
|
||||
// 根据maxNum获取
|
||||
if (!state.channelList || !state.channelList.length) return
|
||||
const playList = state.channelList.slice(0, state.currentNum)
|
||||
for (let index = 0; index < playList.length; index++) {
|
||||
const element: any = playList[index];
|
||||
const _data = {
|
||||
...state.queryForm,
|
||||
channelId: element.channelCode
|
||||
}
|
||||
// 获取回放列表
|
||||
const recordRes = await AlarmReplayVideoListApi.defaultReplayVideoList(_data)
|
||||
if (recordRes.code != 0) continue
|
||||
const videoRes = await AlarmReplayVideoListApi.getRtspReplayVideo(_data)
|
||||
if (videoRes.code != 0) continue
|
||||
console.log("data:", videoRes.data);
|
||||
replayVideo.recordByUrl({
|
||||
wsURL: videoRes.data.wsURL,
|
||||
rtspURL: videoRes.data.fullUrl,
|
||||
channelId: videoRes.data.channelId, // 必传, 通道id
|
||||
records: recordRes.data || [], // 录像列表
|
||||
selectIndex: index, // 必传
|
||||
playRecordByTime: true, // 默认传true,拖拽异常时请传入false
|
||||
startTime: state.startTimestamp, // 开始时间戳(秒)
|
||||
endTime: state.endTimestamp, // 结束时间戳(秒)
|
||||
// playerAdapter: opt.playerAdapter, // 选传, , selfAdaption 自适应 | stretching 拉伸
|
||||
channelData: {
|
||||
records: recordRes.data || []
|
||||
}, // 选传, 通道信息
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁
|
||||
function destroy() {
|
||||
if (!replayVideo) return
|
||||
if (replayVideo) {
|
||||
for (let index = 0; index < state.currentNum; index++) {
|
||||
replayVideo.close(index)
|
||||
}
|
||||
}
|
||||
replayVideo = null;
|
||||
}
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
init
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<draggable-org-Tree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含" :defaultTreeWidth="20"
|
||||
:nodeClickable="false">
|
||||
<template #default>
|
||||
<div class="flex flex-col h-full">
|
||||
<el-row shadow="hover" v-show="showSearch" class="ml10">
|
||||
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
|
||||
<el-form-item style="width: 350px;" label="录像时间段" prop="videoDateList">
|
||||
<el-date-picker v-model="state.queryForm.videoDateList" value-format="YYYY-MM-DD" type="daterange"
|
||||
:disabled-date="disabledDate" placeholder="请选择日期范围" start-placeholder="开始日期" end-placeholder="结束日期"
|
||||
@change="handleDateChange" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px;" label="码流" prop="streamType">
|
||||
<dict-select v-model="state.queryForm.streamType" :options="dh_video_stream_type"
|
||||
placeholder="请选择码流" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px;" label="录像来源" prop="recordSource">
|
||||
<dict-select v-model="state.queryForm.recordSource" :options="dh_replay_source"
|
||||
placeholder="请选择录像来源" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="replayVideoMounted">
|
||||
{{ $t('common.queryBtn') }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
<replay-video class="flex-1 overflow-hidden" ref="replayVideoRef" @mounted="replayVideoMounted" />
|
||||
</div>
|
||||
</template>
|
||||
</draggable-org-Tree>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="systemDeviceCameraReplay">
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table";
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message";
|
||||
import { useDict } from '/@/hooks/dict';
|
||||
|
||||
import * as DeviceCameraReplayApi from "/@/api/third/deviceCameraReplay";
|
||||
import * as DeviceChannelApi from "/@/api/device/deviceChannel"
|
||||
|
||||
// 引入组件
|
||||
const ReplayVideo = defineAsyncComponent(() => import('../components/replay-video/index.vue'));
|
||||
|
||||
// 定义查询字典
|
||||
const { dh_replay_source, dh_video_stream_type } = useDict("dh_replay_source", "dh_video_stream_type")
|
||||
|
||||
// 定义变量内容
|
||||
const draggableDepTreeRef = ref()
|
||||
const replayVideoRef = ref()
|
||||
// 搜索变量
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
// 多选变量
|
||||
const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
|
||||
const state = reactive<any>({
|
||||
queryForm: {
|
||||
ownerCode: null,
|
||||
orgCodes: null,
|
||||
isInclude: null,
|
||||
streamType: "1",
|
||||
recordSource: "1",
|
||||
videoDateList: getDefaultDateRange()
|
||||
},
|
||||
dataList: []
|
||||
})
|
||||
|
||||
const getDataList = async () => {
|
||||
const { data } = await DeviceChannelApi.getReplayList(state.queryForm)
|
||||
if (data && data.length) state.dataList = data
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDataList()
|
||||
})
|
||||
|
||||
const replayVideoMounted = async () => {
|
||||
await getDataList()
|
||||
if (replayVideoRef.value) {
|
||||
replayVideoRef.value.init(state.dataList, state.queryForm)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取默认日期范围(前三天)
|
||||
function getDefaultDateRange() {
|
||||
const today = new Date()
|
||||
const threeDaysAgo = new Date(today.getTime() - 2 * 24 * 60 * 60 * 1000) // 前三天(包含今天)
|
||||
const formatDate = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
return [formatDate(threeDaysAgo), formatDate(today)]
|
||||
}
|
||||
|
||||
// 禁用日期的逻辑
|
||||
const disabledDate = (time: Date) => {
|
||||
const today = new Date()
|
||||
today.setHours(23, 59, 59, 999) // 设置为今天的最后一刻
|
||||
// 禁用未来日期
|
||||
if (time.getTime() > today.getTime()) {
|
||||
return true
|
||||
}
|
||||
// 如果已经选择了开始日期,限制结束日期最多3天
|
||||
const selectedDates = state.queryForm.videoDateList
|
||||
if (selectedDates && selectedDates.length === 1) {
|
||||
const startDate = new Date(selectedDates[0])
|
||||
const maxEndDate = new Date(startDate.getTime() + 2 * 24 * 60 * 60 * 1000) // 最多3天
|
||||
const minEndDate = new Date(startDate.getTime() - 2 * 24 * 60 * 60 * 1000) // 最少3天前
|
||||
|
||||
return time.getTime() > maxEndDate.getTime() || time.getTime() < minEndDate.getTime()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 处理日期变化
|
||||
const handleDateChange = (dates: string[]) => {
|
||||
if (dates && dates.length === 2) {
|
||||
const startDate = new Date(dates[0])
|
||||
const endDate = new Date(dates[1])
|
||||
const diffTime = Math.abs(endDate.getTime() - startDate.getTime())
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1 // 包含起始日期
|
||||
|
||||
if (diffDays > 3) {
|
||||
useMessage().warning('最多只能选择3天的时间范围')
|
||||
// 重置为默认值
|
||||
state.queryForm.videoDateList = getDefaultDateRange()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
// 清空搜索条件
|
||||
queryRef.value?.resetFields()
|
||||
// 重置日期为默认前三天
|
||||
state.queryForm.videoDateList = getDefaultDateRange()
|
||||
// 重置码流为默认值
|
||||
state.queryForm.streamType = "1"
|
||||
// 重置录像来源为默认值
|
||||
state.queryForm.recordSource = "1"
|
||||
// 清空多选
|
||||
selectObjs.value = []
|
||||
draggableDepTreeRef.value.resetData()
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 树节点选择时
|
||||
const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepTree: boolean) => {
|
||||
state.queryForm.ownerCode = curClickedNode.id
|
||||
state.queryForm.orgCodes = checkedTreeNodes.map((item: any) => item.id)
|
||||
state.queryForm.isInclude = !isDeepTree
|
||||
getDataList()
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -10,12 +10,12 @@
|
||||
<el-form-item style="width: 250px;" label="设备名" prop="deviceName">
|
||||
<el-input v-model="state.queryForm.deviceName" placeholder="请输入设备名" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px;" label="设备类型" prop="deviceCategory">
|
||||
<dict-select v-model="state.queryForm.deviceCategory" :options="device_category" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px;" label="状态" prop="isOnline">
|
||||
<dict-select v-model="state.queryForm.isOnline" :options="is_nline" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px;" label="同步状态" prop="isSync">
|
||||
<dict-select v-model="state.queryForm.isSync" :options="is_sync" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="getDataList">
|
||||
{{ $t('common.queryBtn') }}
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row shadow="hover" v-show="showSearch" class="ml10">
|
||||
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
|
||||
<el-form-item style="width: 250px;" label="姓名" prop="name">
|
||||
<el-input v-model="state.queryForm.name" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px;" label="手机号" prop="phone">
|
||||
<el-input v-model="state.queryForm.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px;" label="证件号码" prop="paperNumber">
|
||||
<el-input v-model="state.queryForm.paperNumber" placeholder="请输入证件号码" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="getDataList">
|
||||
{{ $t('common.queryBtn') }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()"
|
||||
|
||||
Reference in new issue
Block a user