From 311a3a36befe7660580b317a52f8385342853a4a Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Tue, 17 Mar 2026 18:28:17 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=9F=A5=E8=AF=A2=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/yt.png | Bin 0 -> 5341 bytes src/components/Select/SelectAreaType.vue | 120 ++++++ src/components/Select/SelectPersonCarType.vue | 57 +++ src/components/index.ts | 4 + .../rtsp-plugin-video/lib/panTilt.css | 167 ++++++++ .../rtsp-plugin-video/lib/panTilt.js | 280 +++++++++++++ .../rtsp-plugin-video/lib/svg/arrow-b.svg | 9 + .../rtsp-plugin-video/lib/svg/arrow-l.svg | 9 + .../rtsp-plugin-video/lib/svg/arrow-lb.svg | 10 + .../rtsp-plugin-video/lib/svg/arrow-lt.svg | 10 + .../rtsp-plugin-video/lib/svg/arrow-r.svg | 9 + .../rtsp-plugin-video/lib/svg/arrow-rb.svg | 10 + .../rtsp-plugin-video/lib/svg/arrow-rt.svg | 10 + .../rtsp-plugin-video/lib/svg/arrow-t.svg | 9 + .../rtsp-plugin-video/lib/svg/ptz-icon1.svg | 4 + .../rtsp-plugin-video/lib/svg/ptz-icon2.svg | 13 + .../rtsp-plugin-video/lib/svg/ptz-icon3.svg | 10 + .../rtsp-plugin-video/lib/svg/ptz-icon4.svg | 10 + .../rtsp-plugin-video/lib/svg/ptz-icon5.svg | 12 + .../rtsp-plugin-video/lib/svg/ptz-icon6.svg | 14 + .../rtsp-plugin-video/video-plugin.vue | 140 ++++++- src/components/rtsp-plugin-video/yt.vue | 389 ++++++++++++++++++ src/utils/dispose.ts | 37 ++ src/views/car/carAccess/index.vue | 13 +- src/views/car/carPark/index.vue | 7 + src/views/car/carRecord/index.vue | 3 +- src/views/door/doorRecord/index.vue | 14 +- src/views/report/door/index.vue | 8 +- 28 files changed, 1353 insertions(+), 25 deletions(-) create mode 100644 public/yt.png create mode 100644 src/components/Select/SelectAreaType.vue create mode 100644 src/components/Select/SelectPersonCarType.vue create mode 100755 src/components/rtsp-plugin-video/lib/panTilt.css create mode 100755 src/components/rtsp-plugin-video/lib/panTilt.js create mode 100755 src/components/rtsp-plugin-video/lib/svg/arrow-b.svg create mode 100755 src/components/rtsp-plugin-video/lib/svg/arrow-l.svg create mode 100755 src/components/rtsp-plugin-video/lib/svg/arrow-lb.svg create mode 100755 src/components/rtsp-plugin-video/lib/svg/arrow-lt.svg create mode 100755 src/components/rtsp-plugin-video/lib/svg/arrow-r.svg create mode 100755 src/components/rtsp-plugin-video/lib/svg/arrow-rb.svg create mode 100755 src/components/rtsp-plugin-video/lib/svg/arrow-rt.svg create mode 100755 src/components/rtsp-plugin-video/lib/svg/arrow-t.svg create mode 100755 src/components/rtsp-plugin-video/lib/svg/ptz-icon1.svg create mode 100755 src/components/rtsp-plugin-video/lib/svg/ptz-icon2.svg create mode 100755 src/components/rtsp-plugin-video/lib/svg/ptz-icon3.svg create mode 100755 src/components/rtsp-plugin-video/lib/svg/ptz-icon4.svg create mode 100755 src/components/rtsp-plugin-video/lib/svg/ptz-icon5.svg create mode 100755 src/components/rtsp-plugin-video/lib/svg/ptz-icon6.svg create mode 100644 src/components/rtsp-plugin-video/yt.vue create mode 100644 src/utils/dispose.ts diff --git a/public/yt.png b/public/yt.png new file mode 100644 index 0000000000000000000000000000000000000000..b0c00a8cac43739e6727388dfcac55c938dd38fe GIT binary patch literal 5341 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 v**O&WKL7v#|No9TGbsQ700v1!K~w_(g + + +
+
{{ v.name }}
+
+ +
+
+
+
+ + + 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">