diff --git a/public/yt.png b/public/yt.png new file mode 100644 index 0000000..b0c00a8 Binary files /dev/null and b/public/yt.png differ diff --git a/src/components/Select/SelectAreaType.vue b/src/components/Select/SelectAreaType.vue new file mode 100644 index 0000000..aa1f42a --- /dev/null +++ b/src/components/Select/SelectAreaType.vue @@ -0,0 +1,120 @@ + + + diff --git a/src/components/Select/SelectPersonCarType.vue b/src/components/Select/SelectPersonCarType.vue new file mode 100644 index 0000000..b594065 --- /dev/null +++ b/src/components/Select/SelectPersonCarType.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/components/index.ts b/src/components/index.ts index 6c80ed4..b308f46 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -35,6 +35,8 @@ import SelectTreeChannel from "/@/components/Select/SelectTreeChannel.vue" import SelectTreeChannelRadio from "/@/components/Select/SelectTreeChannelRadio.vue" import SelectTreeDept from "/@/components/Select/SelectTreeDept.vue" import SelectDeviceTree from "/@/components/Select/SelectDeviceTree.vue" +import SelectAreaType from "/@/components/Select/SelectAreaType.vue" +import SelectPersonCarType from "/@/components/Select/SelectPersonCarType.vue" import ColumnBtn from "/@/components/Column/ColumnBtn.vue" @@ -99,6 +101,8 @@ export default { app.component('SelectTreeChannelRadio', SelectTreeChannelRadio); app.component('SelectTreeDept', SelectTreeDept); app.component('SelectDeviceTree', SelectDeviceTree); + app.component('SelectAreaType', SelectAreaType); + app.component('SelectPersonCarType', SelectPersonCarType); app.component('ColumnBtn', ColumnBtn); diff --git a/src/components/rtsp-plugin-video/lib/panTilt.css b/src/components/rtsp-plugin-video/lib/panTilt.css new file mode 100755 index 0000000..872f05f --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/panTilt.css @@ -0,0 +1,167 @@ +/** + 云台样式 + */ + .ws-pan-tilt-control{ + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + border: 1px solid #e9ebee; + padding-top: 10px; + position: relative; + } + + .ws-pan-tilt-circle{ + width: 180px; + height: 180px; + background-color: #f5f6f9; + border-radius: 50%; + position: relative; + overflow: hidden; + } + + .ws-pan-tilt-direction-item{ + position: absolute; + width: 90px; + height: 90px; + transform-origin: 100% 100%; + transition: border 0.3s ease; + cursor: pointer; + } + + .ws-pan-tilt-direction-item:nth-child(1){ + transform: rotate(66deg) skew(45deg); + } + .ws-pan-tilt-direction-item:nth-child(1) img{ + width: 16px; + position: absolute; + transform: skew(-45deg) rotate(-66deg) translate(-1px, -8px); + top: 55px; + left: 52px; + } + .ws-pan-tilt-direction-item:nth-child(2){ + transform: rotate(111deg) skew(45deg); + } + .ws-pan-tilt-direction-item:nth-child(2) img{ + width: 16px; + position: absolute; + transform: skew(-45deg) rotate(-111deg) translate(1px, 2px); + top: 55px; + left: 44px; + } + .ws-pan-tilt-direction-item:nth-child(3){ + transform: rotate(156deg) skew(45deg); + } + .ws-pan-tilt-direction-item:nth-child(3) img{ + width: 16px; + position: absolute; + transform: skew(-45deg) rotate(-156deg) translate(0px, 3px); + top: 55px; + left: 44px; + } + .ws-pan-tilt-direction-item:nth-child(4){ + transform: rotate(-159deg) skew(45deg); + } + .ws-pan-tilt-direction-item:nth-child(4) img{ + width: 16px; + position: absolute; + transform: skew(-45deg) rotate(159deg) translate(0px, 1px); + top: 55px; + left: 46px; + } + .ws-pan-tilt-direction-item:nth-child(5){ + transform: rotate(-114deg) skew(45deg); + } + .ws-pan-tilt-direction-item:nth-child(5) img{ + width: 16px; + position: absolute; + transform: skew(-45deg) rotate(114deg) translate(2px, -2px); + top: 51px; + left: 48px; + } + .ws-pan-tilt-direction-item:nth-child(6){ + transform: rotate(-69deg) skew(45deg); + } + .ws-pan-tilt-direction-item:nth-child(6) img{ + width: 16px; + position: absolute; + transform: skew(-45deg) rotate(69deg) translate(2px, 2px); + top: 52px; + left: 50px; + } + .ws-pan-tilt-direction-item:nth-child(7){ + transform: rotate(-24deg) skew(45deg); + } + .ws-pan-tilt-direction-item:nth-child(7) img{ + width: 16px; + position: absolute; + transform: skew(-45deg) rotate(24deg) translate(2px, 2px); + top: 50px; + left: 48px; + } + .ws-pan-tilt-direction-item:nth-child(8){ + transform: rotate(21deg) skew(45deg); + } + .ws-pan-tilt-direction-item:nth-child(8) img{ + width: 16px; + position: absolute; + transform: skew(-45deg) rotate(-21deg) translate(0px, 2px); + top: 52px; + left: 49px; + } + .ws-pan-tilt-inner-circle{ + width: 80px; + height: 80px; + border-radius: 50%; + background-color: white; + position: absolute; + top: 28%; + left: 28%; + } + + .cloud-control-wrapper{ + display: flex; + width: 100%; + height: 42px; + align-items: center; + justify-content: center; + border-top: 1px solid #e9ebee; + margin-top: 10px; + } + + .cloud-control-wrapper .ws-pan-tilt-control-item img{ + width: 20px; + height: 20px; + cursor: pointer; + margin: 0 8px; + } + + .cloud-control-wrapper .cloud-control-separate{ + border-left: 1px solid #e9ebee; + height: 20px; + } + .ws-pan-tilt-mask { + position: absolute; + z-index: 100; + cursor: not-allowed; + background-color: #ccc; + opacity: 0.2; + } + .ws-pan-tilt-mask-direction { + top: 0; + width: 100%; + height: calc(100% - 42px); + } + .ws-pan-tilt-mask-zoom { + left: 0; + bottom: 0; + height: 42px; + width: calc(50% + 36px); + } + .ws-pan-tilt-mask-aperture { + right: 0; + bottom: 0; + height: 42px; + width: calc(50% - 36px); + } + \ No newline at end of file diff --git a/src/components/rtsp-plugin-video/lib/panTilt.js b/src/components/rtsp-plugin-video/lib/panTilt.js new file mode 100755 index 0000000..5bb9ea8 --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/panTilt.js @@ -0,0 +1,280 @@ +const arrowT = new URL('./svg/arrow-t.svg', import.meta.url).href +const arrowRt = new URL('./svg/arrow-rt.svg', import.meta.url).href +const arrowR = new URL('./svg/arrow-r.svg', import.meta.url).href +const arrowRb = new URL('./svg/arrow-rb.svg', import.meta.url).href +const arrowB = new URL('./svg/arrow-b.svg', import.meta.url).href +const arrowLb = new URL('./svg/arrow-lb.svg', import.meta.url).href +const arrowL = new URL('./svg/arrow-l.svg', import.meta.url).href +const arrowLt = new URL('./svg/arrow-lt.svg', import.meta.url).href +const ptzIcon1 = new URL('./svg/ptz-icon1.svg', import.meta.url).href +const ptzIcon2 = new URL('./svg/ptz-icon2.svg', import.meta.url).href +const ptzIcon3 = new URL('./svg/ptz-icon3.svg', import.meta.url).href +const ptzIcon4 = new URL('./svg/ptz-icon4.svg', import.meta.url).href +const ptzIcon5 = new URL('./svg/ptz-icon5.svg', import.meta.url).href +const ptzIcon6 = new URL('./svg/ptz-icon6.svg', import.meta.url).href + +class PanTilt { + constructor(options = {}) { + // 云台节点 + this.el = options.el; + // 云台元素 + this.$el = document.querySelector('#' + this.el) + if (this.$el) { + this.__createPanTilt(); + } + // 选中的窗口的正在播放的通道 + this.channel = null; + // 相关的请求接口方法 + // 云台方向控制接口 + this.setPtzDirection = options.setPtzDirection; + // 云台镜头控制接口 + this.setPtzCamera = options.setPtzCamera; + this.clickDirectFlag = false; + this.setPtzDirectionPromiseList = []; + this.setPtzDirectionRun = false; + this.setPtzCameraPromiseList = []; + this.setPtzCameraRun = false + } + + /** + * 设置通道 + * @param channel + */ + setChannel(channel) { + this.channel = { ...channel }; + if (!channel) { + // 通道不存在,则禁用云台 + document.querySelector(`#${this.el} .ws-pan-tilt-mask`).style.display = 'block' + return; + } + let capability = channel.capability; + switch (String(channel.cameraType)) { + case "1": // 枪机通道 + // 枪机通道能力集有电动聚焦或者云台控制时,开放变倍、变焦 + if (parseInt(capability, 2) & parseInt("100", 2) + || parseInt(capability, 2) & parseInt("10000000000000000", 2) + ) { + document.querySelector(`#${this.el} .ws-pan-tilt-mask-zoom`).style.display = 'none' + } else { + document.querySelector(`#${this.el} .ws-pan-tilt-mask-zoom`).style.display = 'block' + } + + // 枪机通道能力集有云台控制时,开放方向控制 + if (parseInt(capability, 2) & parseInt("10000000000000000", 2)) { + document.querySelector(`#${this.el} .ws-pan-tilt-mask-direction`).style.display = 'none' + } else { + document.querySelector(`#${this.el} .ws-pan-tilt-mask-direction`).style.display = 'block' + } + document.querySelector(`#${this.el} .ws-pan-tilt-mask-aperture`).style.display = 'block' + break; + case "2": // 球机通道 + // 球机通道,可以使用云台所有功能 + document.querySelector(`#${this.el} .ws-pan-tilt-mask`).style.display = 'none' + document.querySelector(`#${this.el} .ws-pan-tilt-mask-zoom`).style.display = 'none' + document.querySelector(`#${this.el} .ws-pan-tilt-mask-aperture`).style.display = 'none' + break; + default: + // 默认禁用云台 + document.querySelector(`#${this.el} .ws-pan-tilt-mask`).style.display = 'block' + document.querySelector(`#${this.el} .ws-pan-tilt-mask-zoom`).style.display = 'block' + document.querySelector(`#${this.el} .ws-pan-tilt-mask-aperture`).style.display = 'block' + } + } + + __createPanTilt() { + this.$el.innerHTML = ` +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+ + + +
+ +
+ +
+
+ ` + // 添加监听 + this.addListener() + + } + + addListener() { + try { + let domList = document.querySelectorAll(`#${this.el} .ws-pan-tilt-direction-item img`) + domList.forEach(dom => { + dom.addEventListener('mouseup', (e) => { + if (this.clickDirectFlag) { + this.clickDirectFlag = false + this.__setPtzDirection.call(this, e.target.getAttribute("direct"), "0") + } + }) + dom.addEventListener('mousedown', this._throttle((e) => { + if (!this.clickDirectFlag) { + this.clickDirectFlag = true + this.__setPtzDirection.call(this, e.target.getAttribute("direct"), "1") + } + })) + dom.addEventListener('mouseout', this._throttle((e) => { + if (this.clickDirectFlag) { + this.clickDirectFlag = false + this.__setPtzDirection.call(this, e.target.getAttribute("direct"), "0") + } + })) + }) + let domList2 = document.querySelectorAll(`#${this.el} .ws-pan-tilt-control-item img`) + domList2.forEach(dom => { + dom.addEventListener('mouseup', (e) => { + if (this.clickDirectFlag) { + this.clickDirectFlag = false + this.__setPtzCamera.call(this, e.target.getAttribute("operateType"), e.target.getAttribute("direct"), "0") + } + }) + dom.addEventListener('mousedown', this._throttle((e) => { + if (!this.clickDirectFlag) { + this.clickDirectFlag = true + this.__setPtzCamera.call(this, e.target.getAttribute("operateType"), e.target.getAttribute("direct"), "1") + } + })) + dom.addEventListener('mouseout', this._throttle((e) => { + if (this.clickDirectFlag) { + this.clickDirectFlag = false + this.__setPtzCamera.call(this, e.target.getAttribute("operateType"), e.target.getAttribute("direct"), "0") + } + })) + }) + } catch (err) { + let timer = setTimeout(() => { + this.addListener() + clearTimeout(timer) + }, 300) + } + } + + __setPtzDirection(direct, command) { + // 方向:1=上,2=下,3=左,4=右,5=左上,6=左下,7=右上,8=右下 + const params = { + project: 'PSDK', + method: 'DMS.Ptz.OperateDirect', + data: { + direct, + command, + stepX: '4', + stepY: '4', + channelId: this.channel.id + } + }; + this.setPtzDirectionPromiseList.push(() => { + return new Promise((resolve, reject) => { + this.setPtzDirection && this.setPtzDirection(params).then(() => { + resolve() + }).catch(err => { + reject(err) + }) + }) + }) + if (!this.setPtzDirectionRun) { + let reqQueue = () => { + this.setPtzDirectionRun = true + this.setPtzDirectionPromiseList[0]().then(() => { + }).catch((err) => { + console.error('云台方向控制err:', err); + }).finally(() => { + this.setPtzDirectionPromiseList.shift() + if (!this.setPtzDirectionPromiseList.length) { + this.setPtzDirectionRun = false + } else { + reqQueue() + } + }) + } + reqQueue() + } + } + + __setPtzCamera(operateType, direct, command) { + // operateType 操作类型:1=变倍,2=变焦,3=光圈 + // direct 方向:1=增加,2=减小 + // command 命令:0=停止,1=开启 + const params = { + project: 'PSDK', + method: 'DMS.Ptz.OperateCamera', + data: { + operateType, + direct, + command, + step: '4', + channelId: this.channel.id + } + }; + + + this.setPtzCameraPromiseList.push(() => { + return new Promise((resolve, reject) => { + this.setPtzCamera && this.setPtzCamera(params).then(() => { + resolve() + }).catch(err => { + reject(err) + }) + }) + }) + if (!this.setPtzCameraRun) { + let reqQueue = () => { + this.setPtzCameraRun = true + this.setPtzCameraPromiseList[0]().then(() => { + }).catch((err) => { + console.error('云台方向控制err:', err); + }).finally(() => { + this.setPtzCameraPromiseList.shift() + if (!this.setPtzCameraPromiseList.length) { + this.setPtzCameraRun = false + } else { + reqQueue() + } + }) + } + reqQueue() + } + } + + _throttle(func, delay = 300) { + let lastCall = 0; + return function (...args) { + const now = new Date().getTime(); + if (now - lastCall < delay) { + return; + } + lastCall = now; + return func.apply(this, args); + } + } +} + +export default PanTilt diff --git a/src/components/rtsp-plugin-video/lib/svg/arrow-b.svg b/src/components/rtsp-plugin-video/lib/svg/arrow-b.svg new file mode 100755 index 0000000..7105c60 --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/svg/arrow-b.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/src/components/rtsp-plugin-video/lib/svg/arrow-l.svg b/src/components/rtsp-plugin-video/lib/svg/arrow-l.svg new file mode 100755 index 0000000..daa5902 --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/svg/arrow-l.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/src/components/rtsp-plugin-video/lib/svg/arrow-lb.svg b/src/components/rtsp-plugin-video/lib/svg/arrow-lb.svg new file mode 100755 index 0000000..facc6b9 --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/svg/arrow-lb.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/components/rtsp-plugin-video/lib/svg/arrow-lt.svg b/src/components/rtsp-plugin-video/lib/svg/arrow-lt.svg new file mode 100755 index 0000000..b19eed1 --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/svg/arrow-lt.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/components/rtsp-plugin-video/lib/svg/arrow-r.svg b/src/components/rtsp-plugin-video/lib/svg/arrow-r.svg new file mode 100755 index 0000000..42ffd1a --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/svg/arrow-r.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/src/components/rtsp-plugin-video/lib/svg/arrow-rb.svg b/src/components/rtsp-plugin-video/lib/svg/arrow-rb.svg new file mode 100755 index 0000000..50adf01 --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/svg/arrow-rb.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/components/rtsp-plugin-video/lib/svg/arrow-rt.svg b/src/components/rtsp-plugin-video/lib/svg/arrow-rt.svg new file mode 100755 index 0000000..21d8048 --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/svg/arrow-rt.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/components/rtsp-plugin-video/lib/svg/arrow-t.svg b/src/components/rtsp-plugin-video/lib/svg/arrow-t.svg new file mode 100755 index 0000000..c20ce8c --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/svg/arrow-t.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/src/components/rtsp-plugin-video/lib/svg/ptz-icon1.svg b/src/components/rtsp-plugin-video/lib/svg/ptz-icon1.svg new file mode 100755 index 0000000..709449b --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/svg/ptz-icon1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/rtsp-plugin-video/lib/svg/ptz-icon2.svg b/src/components/rtsp-plugin-video/lib/svg/ptz-icon2.svg new file mode 100755 index 0000000..fb1e914 --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/svg/ptz-icon2.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/src/components/rtsp-plugin-video/lib/svg/ptz-icon3.svg b/src/components/rtsp-plugin-video/lib/svg/ptz-icon3.svg new file mode 100755 index 0000000..fed521e --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/svg/ptz-icon3.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/components/rtsp-plugin-video/lib/svg/ptz-icon4.svg b/src/components/rtsp-plugin-video/lib/svg/ptz-icon4.svg new file mode 100755 index 0000000..c0ff7b8 --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/svg/ptz-icon4.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/components/rtsp-plugin-video/lib/svg/ptz-icon5.svg b/src/components/rtsp-plugin-video/lib/svg/ptz-icon5.svg new file mode 100755 index 0000000..e36190a --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/svg/ptz-icon5.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/src/components/rtsp-plugin-video/lib/svg/ptz-icon6.svg b/src/components/rtsp-plugin-video/lib/svg/ptz-icon6.svg new file mode 100755 index 0000000..8541a5d --- /dev/null +++ b/src/components/rtsp-plugin-video/lib/svg/ptz-icon6.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/components/rtsp-plugin-video/video-plugin.vue b/src/components/rtsp-plugin-video/video-plugin.vue index 00fa542..faea30c 100644 --- a/src/components/rtsp-plugin-video/video-plugin.vue +++ b/src/components/rtsp-plugin-video/video-plugin.vue @@ -8,7 +8,11 @@
+ 云台
+ + + @@ -21,7 +25,9 @@ import * as VideoPlayer from "./lib/videoPlayer"; import * as settingApi from "/@/api/com/comDhSetting" -const emit = defineEmits(["changePage"]) +const Yt = defineAsyncComponent(() => import("./yt.vue")); + +const emit = defineEmits(["changePage", "changeSetting"]) const TAG = "|视频预览|" @@ -46,6 +52,13 @@ let lastRealSignature = "" let lastRealStartAt = 0 let lastPlaybackSignature = "" let lastPlaybackStartAt = 0 +let currentSnum = null + +const ytRef = ref() + +const ytState = reactive({ + title: "", +}) const pageCurrent = ref(1) const pageSize = ref(9) @@ -133,6 +146,7 @@ function clampByDivision(list) { } function applyDivisionByCount(count) { + if (!props.autoApplyDivision) return if (!VIDEO_PLAYER?.changeDivision) return const nextDivision = getDivisionByCount(count) const currentDivision = Number(VIDEO_PLAYER?.setting?.division) @@ -231,10 +245,14 @@ function flushReadyQueue() { * - 提供登录信息与播放器基础参数,支持父组件通过 ref 调用预览与回放方法 */ const props = defineProps({ + /** 是否展示云台 */ + visibleYt: { type: Boolean, default: true }, /** 播放器类型:0-实时预览,3-录像回放,7-录像回放(支持倒放) */ windowType: { type: Number, default: 0 }, /** 默认展示的窗口数量 */ division: { type: Number, default: 1 }, + /** 是否自动应用分屏配置 */ + autoApplyDivision: { type: Boolean, default: true }, /** * 分屏配置 * 全部配置[1, 2, 4, 6, 8, 9, 13, 16, 20, 25, 36, 64] @@ -308,7 +326,7 @@ async function initVideoPlayer(type = props.windowType) { // 底部操作栏, 选传,【true - 显示, false - 隐藏】 showBar: props.showBar, // 如果DOM元素被插件挡住了,把DOM元素的类名传入。 - shieldClass: ["el-select__popper", "el-popper", "video-container-page"], + shieldClass: ["el-select__popper", "el-popper", "video-container-page", "yt_info_popup"], // 如果插件要在dom内滚动,需要把DOM元素的类名传入,请查看案例-遮挡 coverShieldClass: [], // 有 iframe 时,top层 的 dom 元素被插件挡住了,把DOM元素的类名传入。 @@ -415,10 +433,14 @@ async function initVideoPlayer(type = props.windowType) { // 关闭视频窗口回调 closeWindowSuccess: ({ isAll, snum, channelList }) => { console.log(TAG, "关闭视频窗口,isAll:", isAll, "snum:", snum, "channelList:", channelList) + emitSetting() }, // 鼠标单击窗口回调 clickWindow: (snum) => { console.log(TAG, "鼠标单击窗口,snum:", snum) + currentSnum = snum + emitSetting() + clickWindowCallback() }, // 鼠标双击窗口回调 dbClickWindow: (snum) => { @@ -431,10 +453,12 @@ async function initVideoPlayer(type = props.windowType) { // rtsp 流下载录像成功回调 downloadRecordSuccess: (info) => { console.log(TAG, "RTSP录像下载成功,info:", info) + ElMessage.success(info.message) }, // rtsp 流下载录像失败回调 downloadRecordError: (info, err) => { console.log(TAG, "RTSP录像下载失败,info:", info, "err:", err) + ElMessage.error("RTSP录像下载失败") } }) currentWindowType.value = type @@ -526,11 +550,11 @@ async function startPlayback(items) { lastPlaybackStartAt = now applyDivisionByCount(Array.isArray(items) ? items.length : 1) console.log(TAG, "开始录像回放,items:", items) - try { - await closeVideo() - } catch (e) { - console.log(TAG, "开始回放前关闭旧视频失败,err:", e) - } + // try { + // await closeVideo() + // } catch (e) { + // console.log(TAG, "开始回放前关闭旧视频失败,err:", e) + // } VIDEO_PLAYER.startPlayback(items) loading.value = false } @@ -604,6 +628,39 @@ function saveSnapshot(base64Url, filename) { } } +function toLocalFileUrl(p) { + const s = String(p || "") + if (s.startsWith("file://")) return s + if (/^[a-zA-Z]:/.test(s)) { + const fixed = s.replace(/\\/g, "/") + return `file:///${encodeURI(fixed)}` + } + return s +} + +function tryCopyText(text) { + const t = String(text ?? "") + if (!t) return Promise.resolve(false) + if (navigator.clipboard?.writeText) { + return navigator.clipboard.writeText(t).then(() => true).catch(() => false) + } + try { + const input = document.createElement("input") + input.value = t + input.style.position = "fixed" + input.style.left = "-9999px" + input.style.top = "0" + document.body.appendChild(input) + input.focus() + input.select() + const ok = document.execCommand("copy") + document.body.removeChild(input) + return Promise.resolve(!!ok) + } catch (e) { + return Promise.resolve(false) + } +} + /** * 保存视频到本地(可选) * @param {string} path 视频路径 @@ -618,12 +675,20 @@ function saveVideo(path, filename) { const p = String(path) const isLocal = p.startsWith("file://") || /^[a-zA-Z]:/.test(p) if (isLocal) { - console.log(TAG, "无法通过浏览器下载本地文件,路径:", p) - if (navigator.clipboard?.writeText) { - navigator.clipboard.writeText(p) - .then(() => console.log(TAG, "已复制到剪贴板,请在资源管理器打开")) - .catch((e) => console.log(TAG, "复制到剪贴板失败,err:", e)) + const fileUrl = toLocalFileUrl(p) + let opened = false + try { + opened = !!window.open(fileUrl, "_blank") + } catch (e) { + opened = false } + tryCopyText(p).then((copied) => { + if (opened) { + ElMessage.success(`已尝试打开本地录像:${p}${copied ? "(路径已复制)" : ""}`) + } else { + ElMessage.warning(`录像已下载到本地:${p}${copied ? "(路径已复制,可在资源管理器粘贴打开)" : "(请在资源管理器打开)"}`) + } + }) return } const a = document.createElement("a") @@ -666,6 +731,41 @@ function setTotal(total) { } } +function emitSetting() { + emit("changeSetting", { + ...VIDEO_PLAYER.setting, + currentSnum, + }) +} + +function clickWindowCallback() { + if (props.visibleYt && currentWindowType == 0) { + updateYtChannel() + } +} + +/* ************************************* start 云台相关 start ************************************* */ + +function visibleYt() { + console.log("visibleYt,currentSnum:", currentSnum); + if (currentSnum !== null && currentSnum !== undefined) { + VIDEO_PLAYER.setting + } + ytRef.value.open() +} + +const updateYtChannel = () => { + if (!ytRef.value?.isVisible?.()) { + return + } + if (currentSnum !== null && currentSnum !== undefined) { + ytState.title = "" + ytRef.value.setChannel({}) + } +} + +/* ************************************* end 云台相关 end ************************************* */ + onMounted(() => { document.addEventListener("visibilitychange", handleVisibilityChange) }) @@ -740,13 +840,23 @@ defineExpose({ .video-container-page { position: absolute; pointer-events: auto; + background: #2F3236; + border-radius: 20px; + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } :deep(.video-pager) { - background: #2F3236; - border-radius: 20px; padding: 6px 10px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +:deep(.video-yt) { + width: 25px; + height: 25px; + margin-left: 5px; + cursor: pointer; } :deep(.el-pagination) { diff --git a/src/components/rtsp-plugin-video/yt.vue b/src/components/rtsp-plugin-video/yt.vue new file mode 100644 index 0000000..8a3bc1b --- /dev/null +++ b/src/components/rtsp-plugin-video/yt.vue @@ -0,0 +1,389 @@ + + + + + diff --git a/src/utils/dispose.ts b/src/utils/dispose.ts new file mode 100644 index 0000000..926f7ee --- /dev/null +++ b/src/utils/dispose.ts @@ -0,0 +1,37 @@ +function parseDate(input: string) { + const normalized = input.replace(/\//g, "-").replace(" ", "T"); + const d = new Date(normalized); + if (!isNaN(d.getTime())) return d; + const m = input.match( + /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/ + ); + if (m) { + const y = Number(m[1]); + const mo = Number(m[2]) - 1; + const da = Number(m[3]); + const h = Number(m[4]); + const mi = Number(m[5]); + const se = m[6] ? Number(m[6]) : 0; + return new Date(y, mo, da, h, mi, se); + } + return new Date(input); +} + +export function timeDiff(enterTime: string, exitTime: string) { + const enter = parseDate(enterTime); + const exit = parseDate(exitTime); + let diff = exit.getTime() - enter.getTime(); + if (diff < 0) diff = 0; + const totalMinutes = Math.floor(diff / (1000 * 60)); + const totalHours = Math.floor(totalMinutes / 60); + const diffMinutes = totalMinutes % 60; + const diffDays = Math.floor(totalHours / 24); + const diffHours = totalHours % 24; + if (diffDays > 0) { + if (diffMinutes === 0) return `${diffDays}天${diffHours}小时`; + return `${diffDays}天${diffHours}小时${diffMinutes}分钟`; + } + if (diffHours === 0) return `${diffMinutes}分钟`; + if (diffMinutes === 0) return `${diffHours}小时`; + return `${diffHours}小时${diffMinutes}分钟`; +} diff --git a/src/views/car/carAccess/index.vue b/src/views/car/carAccess/index.vue index 394b18a..f6053ed 100644 --- a/src/views/car/carAccess/index.vue +++ b/src/views/car/carAccess/index.vue @@ -7,6 +7,9 @@ + + + @@ -77,6 +80,13 @@ + + + + @@ -91,13 +101,14 @@ import { BasicTableProps, useTable } from "/@/hooks/table"; import { useMessage, useMessageBox } from "/@/hooks/message"; import { useDict } from '/@/hooks/dict'; +import { timeDiff } from '/@/utils/dispose'; import * as CarAccessApi from "/@/api/car/carAccess"; // 引入组件 const FormDialog = defineAsyncComponent(() => import('./form.vue')); // 定义查询字典 -const { dh_enter_Mode } = useDict('dh_enter_Mode'); +const { dh_enter_Mode, yes_no_type } = useDict('dh_enter_Mode', 'yes_no_type'); // 定义变量内容 const formDialogRef = ref() diff --git a/src/views/car/carPark/index.vue b/src/views/car/carPark/index.vue index cc3d6c3..8a7ad79 100644 --- a/src/views/car/carPark/index.vue +++ b/src/views/car/carPark/index.vue @@ -56,6 +56,12 @@ 未出场 + + + @@ -70,6 +76,7 @@ import * as carParkApi from "/@/api/car/carPark"; import { useMessage, useMessageBox } from "/@/hooks/message"; import { useDict } from '/@/hooks/dict'; import { usePanel } from "/@/stores/panel" +import { timeDiff } from '/@/utils/dispose'; const storesPanel = usePanel() diff --git a/src/views/car/carRecord/index.vue b/src/views/car/carRecord/index.vue index 43c063a..3d69287 100644 --- a/src/views/car/carRecord/index.vue +++ b/src/views/car/carRecord/index.vue @@ -60,7 +60,8 @@
未知 - {{ row.carNum }} + {{ row.carNum }} + 未知
diff --git a/src/views/door/doorRecord/index.vue b/src/views/door/doorRecord/index.vue index a8334de..bc04d93 100644 --- a/src/views/door/doorRecord/index.vue +++ b/src/views/door/doorRecord/index.vue @@ -12,6 +12,12 @@ + + + + + + @@ -44,25 +50,25 @@ diff --git a/src/views/report/door/index.vue b/src/views/report/door/index.vue index dd88240..8b89b91 100644 --- a/src/views/report/door/index.vue +++ b/src/views/report/door/index.vue @@ -67,28 +67,28 @@ :label="columnSetting['personCode'].label">