feat:查询条件

This commit is contained in:
dzc committed 2026-03-17 18:28:17 +08:00
1 parent 876e3fe11c
commit 311a3a36be
28 files changed
+1353 -25

No files matched your search

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

+120
View File
@@ -0,0 +1,120 @@
<template>
<el-select style="width: 100%" v-model="state.selectId" :placeholder="props.placeholder"
:clearable="props.clearable" remote-show-suffix filterable remote :disabled="props.disabled"
:remote-method="remoteSearch" :loading="state.loading" @visible-change="(e: boolean) => e && getOptions()"
@change="change">
<el-option v-for="v in state.options" :key="v.uniqueCode" :label="`${v.name}`" :value="v.uniqueCode">
<div class="flex items-center">
<div class="color-primary">{{ v.name }}</div>
<div class="flex items-center justify-center ml5">
<dict-tag :options="state.hardType" :value="v.type" />
</div>
</div>
</el-option>
</el-select>
</template>
<script setup lang="ts" name="select-area-type">
import { reactive } from "vue"
import { useDict } from "/@/hooks/dict"
import * as AreaManageApi from "/@/api/area/areaManage"
const emit = defineEmits(["change"])
const selectId = defineModel()
const AREA_TTYPE = {
car: ["NM", "BM", "DM"],
person: ["NM", "BM", "DM", "BGL"],
}
const props = withDefaults(
defineProps<{
// car | person
areaType?: string
placeholder?: string
disabled?: boolean
clearable?: boolean
autoFirst?: boolean
apiParams?: any
}>(),
{
// car | person
areaType: "car",
placeholder: "请选择",
disabled: false,
clearable: true,
autoFirst: false,
apiParams: {},
},
)
const queryForm = reactive<any>({
name: "",
current: 1,
size: 10,
})
const state = reactive<any>({
loading: true,
selectId: selectId,
options: [],
hardType: [],
})
watch(
selectId,
(newVal, oldValue) => {
if (newVal == oldValue) return
state.selectId = newVal
getOptions()
},
{ immediate: true },
)
async function getOptions() {
try {
state.loading = true
const _pa = {
...queryForm,
...props.apiParams,
}
if (props.areaType && props.areaType == "car") {
_pa.uniqueCodeList = AREA_TTYPE[props.areaType]
}
if (props.areaType && props.areaType == "person") {
_pa.uniqueCodeList = AREA_TTYPE[props.areaType]
}
const { data } = await AreaManageApi.select(_pa)
state.options = data.records ?? []
if (props.autoFirst && state.options.length != 0) {
state.selectId = state.options[0].uniqueCode
emit("change", state.selectId)
}
const { hard_type } = useDict("hard_type")
state.hardType = hard_type
} finally {
setTimeout(() => (state.loading = false), 500)
}
}
function remoteSearch(params: string) {
if (params != queryForm.name) {
queryForm.name = params
getOptions()
}
}
function clear() {
state.selectId = null
getOptions()
}
function change() {
emit("change", selectId)
}
onMounted(() => {
props.autoFirst && getOptions()
})
</script>
@@ -0,0 +1,57 @@
<template>
<dict-select style="width: 100%" v-model="state.selectId" :options="selectOptions" :placeholder="placeholder" />
</template>
<script setup lang="ts" name="select-person-car-type">
import { reactive } from "vue"
import { useDict } from "/@/hooks/dict"
const selectId = defineModel()
const props = withDefaults(
defineProps<{
// car | person
areaType?: string
placeholder?: string
disabled?: boolean
clearable?: boolean
autoFirst?: boolean
apiParams?: any
}>(),
{
// car | person
areaType: "car",
placeholder: "请选择",
disabled: false,
clearable: true,
autoFirst: false,
apiParams: {},
},
)
const { car_type, person_type } = useDict("car_type", "person_type")
const selectOptions = computed(() => {
const list = props.areaType == "car" ? car_type.value : person_type.value
if (!list) {
return []
}
return list
})
const state = reactive<any>({
loading: true,
selectId: selectId,
options: [],
hardType: [],
})
watch(
selectId,
(newVal, oldValue) => {
if (newVal == oldValue) return
state.selectId = newVal
},
{ immediate: true },
)
</script>
+4
View File
@@ -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);
+167
View File
@@ -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);
}
+280
View File
@@ -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 = `
<div class="ws-pan-tilt-control">
<!--云台方向控制-->
<div class="ws-pan-tilt-circle">
<div class="ws-pan-tilt-direction-item"><img src="${arrowT}" title="上" direct="1"/></div>
<div class="ws-pan-tilt-direction-item"><img src="${arrowRt}" title="右上" direct="7"/></div>
<div class="ws-pan-tilt-direction-item"><img src="${arrowR}" title="右" direct="4"/></div>
<div class="ws-pan-tilt-direction-item"><img src="${arrowRb}" title="右下" direct="8"/></div>
<div class="ws-pan-tilt-direction-item"><img src="${arrowB}" title="下" direct="2"/></div>
<div class="ws-pan-tilt-direction-item"><img src="${arrowLb}" title="左下" direct="6"/></div>
<div class="ws-pan-tilt-direction-item"><img src="${arrowL}" title="左" direct="3"/></div>
<div class="ws-pan-tilt-direction-item"><img src="${arrowLt}" title="左上" direct="5"/></div>
<div class="ws-pan-tilt-inner-circle">
<div class="three-circle">
</div>
</div>
</div>
<!--云台变倍、聚焦、光圈控制-->
<div class="cloud-control-wrapper">
<div class="ws-pan-tilt-control-item"><img src="${ptzIcon1}" title="变倍-" operateType="1" direct="2"/></div>
<div class="ws-pan-tilt-control-item"><img src="${ptzIcon2}" title="变倍+" operateType="1" direct="1"/></div>
<div class="cloud-control-separate"></div>
<div class="ws-pan-tilt-control-item"><img src="${ptzIcon3}" title="聚焦-" operateType="2" direct="2"/></div>
<div class="ws-pan-tilt-control-item"><img src="${ptzIcon4}" title="聚焦+" operateType="2" direct="1"/></div>
<div class="cloud-control-separate"></div>
<div class="ws-pan-tilt-control-item"><img src="${ptzIcon5}" title="光圈-" operateType="3" direct="2"/></div>
<div class="ws-pan-tilt-control-item"><img src="${ptzIcon6}" title="光圈+" operateType="3" direct="1"/></div>
</div>
<!--遮罩,当通道没有云台功能时,使用遮罩遮住云台按钮-->
<!--方向按钮遮罩-->
<div class="ws-pan-tilt-mask ws-pan-tilt-mask-direction"></div>
<!--变倍、聚焦遮罩-->
<div class="ws-pan-tilt-mask ws-pan-tilt-mask-zoom"></div>
<!--光圈遮罩-->
<div class="ws-pan-tilt-mask ws-pan-tilt-mask-aperture"></div>
</div>
`
// 添加监听
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
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#657685;}
</style>
<path id="_5-2" class="st0" d="M10.1,15L18,7.1V5.5H2.2v1.6L10.1,15z"/>
</svg>

After

Width:  |  Height:  |  Size: 478 B

+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#657685;}
</style>
<path id="_7-2" class="st0" d="M5.1,10l7.9,7.9h1.6V2.1H13L5.1,10z"/>
</svg>

After

Width:  |  Height:  |  Size: 476 B

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#657685;}
</style>
<path id="_6-2" class="st0" d="M6.9,14.7h6.2c0.9,0,1.6-0.7,1.6-1.6l0,0L6.9,5.3C6,5.3,5.3,6,5.3,6.9l0,0v6.2
C5.3,14,6,14.7,6.9,14.7C6.9,14.7,6.9,14.7,6.9,14.7z"/>
</svg>

After

Width:  |  Height:  |  Size: 570 B

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#657685;}
</style>
<path id="_8-2" class="st0" d="M6.9,5.5h6.2c0.9,0,1.6,0.7,1.6,1.6l0,0l-7.8,7.8c-0.9,0-1.6-0.7-1.6-1.6l0,0V7.1
C5.3,6.2,6,5.5,6.9,5.5C6.9,5.5,6.9,5.5,6.9,5.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 569 B

+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#657685;}
</style>
<path id="_3-2" class="st0" d="M15.1,10l-7.9,7.9H5.6V2.1h1.6L15.1,10z"/>
</svg>

After

Width:  |  Height:  |  Size: 480 B

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#657685;}
</style>
<path id="_4-2" class="st0" d="M13.5,14.7H7.3c-0.9,0-1.6-0.7-1.6-1.6l0,0l7.8-7.8c0.9,0,1.6,0.7,1.6,1.6l0,0v6.2
C15.1,14,14.4,14.7,13.5,14.7C13.5,14.7,13.5,14.7,13.5,14.7z"/>
</svg>

After

Width:  |  Height:  |  Size: 582 B

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#657685;}
</style>
<path id="_2-2" class="st0" d="M13.5,5.5H7.3c-0.9,0-1.6,0.7-1.6,1.6l0,0l7.8,7.8c0.9,0,1.6-0.7,1.6-1.6l0,0V7.1
C15.1,6.2,14.4,5.5,13.5,5.5C13.5,5.5,13.5,5.5,13.5,5.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 577 B

+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#657685;}
</style>
<path id="_1-2" class="st0" d="M10.1,5l7.9,7.9v1.6H2.2v-1.6L10.1,5z"/>
</svg>

After

Width:  |  Height:  |  Size: 478 B

+4
View File
@@ -0,0 +1,4 @@
<svg id="_1" data-name="1" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<rect id="矩形_552" data-name="矩形 552" width="20" height="20" fill="none" opacity="0"/>
<path id="联合_354" data-name="联合 354" d="M-5040.636-9881.895l-2.5-2.5a6.963,6.963,0,0,1-4.192,1.4,7.009,7.009,0,0,1-7-7,7.009,7.009,0,0,1,7-7,7.01,7.01,0,0,1,7,7,6.966,6.966,0,0,1-1.4,4.192l2.5,2.5a1,1,0,0,1,0,1.413,1,1,0,0,1-.707.293A1,1,0,0,1-5040.636-9881.895Zm-11.689-8.1a5.005,5.005,0,0,0,5,5,5.006,5.006,0,0,0,5-5,5.006,5.006,0,0,0-5-5A5.005,5.005,0,0,0-5052.325-9890Zm3.73,1a1,1,0,0,1-1-1,1,1,0,0,1,1-1h3a1,1,0,0,1,1,1,1,1,0,0,1-1,1Z" transform="translate(5056.266 9898.999)" fill="#657685"/>
</svg>

After

Width:  |  Height:  |  Size: 718 B

+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#657685;}
</style>
<path id="联合_355" class="st0" d="M15.6,17.1l-2.5-2.5c-1.2,0.9-2.7,1.4-4.2,1.4c-3.9,0-7-3.1-7-7c0-3.9,3.1-7,7-7
c3.9,0,7,3.1,7,7c0,1.5-0.5,3-1.4,4.2l2.5,2.5c0.4,0.4,0.4,1,0,1.4c-0.2,0.2-0.4,0.3-0.7,0.3C16.1,17.4,15.8,17.3,15.6,17.1z M3.9,9
c0,2.8,2.2,5,5,5c2.8,0,5-2.2,5-5c0-2.8-2.2-5-5-5C6.2,4,3.9,6.2,3.9,9L3.9,9z M8.2,10.5V10H7.7c-0.6,0-1-0.4-1-1s0.4-1,1-1h0.5V7.5
c0-0.6,0.4-1,1-1s1,0.4,1,1V8h0.5c0.6,0,1,0.4,1,1s-0.4,1-1,1h-0.5v0.5c0,0.6-0.4,1-1,1C8.6,11.5,8.2,11.1,8.2,10.5
C8.2,10.5,8.2,10.5,8.2,10.5L8.2,10.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 929 B

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="_3" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#657685;}
</style>
<path id="联合_357" class="st0" d="M15,17h-3v-2h3v-3h2v5H15z M3,17v-5h2v3h3v2H3z M9,10c0-0.6,0.4-1,1-1s1,0.4,1,1s-0.4,1-1,1
C9.5,11,9,10.6,9,10C9,10,9,10,9,10L9,10z M15,8V5h-3V3h5v5H15z M3,8V3h5v2H5v3H3z"/>
</svg>

After

Width:  |  Height:  |  Size: 611 B

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="_4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#657685;}
</style>
<path id="联合_357" class="st0" d="M15,17h-3v-2h3v-3h2v5H15z M3,17v-5h2v3h3v2H3z M8,10c0-1.1,0.9-2,2-2s2,0.9,2,2s-0.9,2-2,2
C8.9,12,8,11.1,8,10C8,10,8,10,8,10L8,10z M15,8V5h-3V3h5v5H15z M3,8V3h5v2H5v3H3z"/>
</svg>

After

Width:  |  Height:  |  Size: 611 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#657685;}
</style>
<path id="联合_358" class="st0" d="M5.2,15.1l2.6-4.6l3.7,6.3C11,16.9,10.5,17,10,17C8.2,17,6.5,16.3,5.2,15.1z M9.9,12.5h6.7
c-0.8,1.9-2.3,3.4-4.3,4.1L9.9,12.5z M3,10c0-0.6,0.1-1.3,0.3-1.9h5.1l-3.7,6.4C3.6,13.2,3,11.6,3,10L3,10z M11.7,11.8l3.6-6.3
C16.4,6.8,17,8.3,17,10c0,0.6-0.1,1.2-0.2,1.8H11.7z M11.3,7.8l-0.2-0.3l-0.1-0.1L8.7,3.1C9.1,3,9.5,3,10,3c1.8,0,3.5,0.7,4.8,1.9
l-2.6,4.4L11.3,7.8z M3.5,7.4c0.8-1.9,2.4-3.4,4.4-4l2.3,4H3.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 849 B

+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#657685;}
</style>
<path id="减去_260" class="st0" d="M10,17c-1.8,0-3.5-0.7-4.8-1.9l1.3-2.3c0.9,1.1,2.2,1.7,3.5,1.7h0.1l1.4,2.3
C11,16.9,10.5,17,10,17z M12.2,16.6L12.2,16.6L11,14.4c1.1-0.2,2.2-0.9,2.8-1.9h2.8C15.8,14.5,14.2,16,12.2,16.6L12.2,16.6z
M4.6,14.5L4.6,14.5C3.6,13.2,3,11.6,3,10c0-0.6,0.1-1.3,0.3-1.9h2.7C5.7,8.7,5.5,9.4,5.5,10c0,0.7,0.2,1.4,0.5,2.1L4.6,14.5
L4.6,14.5z M16.8,11.7h-2.6c0.2-0.6,0.4-1.1,0.4-1.8c0-0.8-0.2-1.5-0.5-2.1l1.4-2.4C16.4,6.8,17,8.3,17,10
C17,10.6,16.9,11.2,16.8,11.7L16.8,11.7z M6.4,7.4H3.5c0.8-1.9,2.4-3.4,4.4-4l1.3,2.3C8.1,5.8,7,6.4,6.4,7.4L6.4,7.4z M13.5,7.1
L13.5,7.1c-0.9-1-2.1-1.6-3.5-1.6L8.7,3.1C9.1,3,9.5,3,10,3c1.8,0,3.5,0.7,4.8,1.9L13.5,7.1L13.5,7.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+125 -15
View File
@@ -8,7 +8,11 @@
<div v-if="pageTotal > 1 && showBar" :style="videoPageStyle" class="video-container-page">
<el-pagination class="video-pager" :current-page="pageCurrent" :page-size="pageSize" :total="pageTotal"
layout="prev, next, total,jumper" small @size-change="onSizeChange" @current-change="onCurrentChange" />
<img v-if="currentWindowType == 0 && visibleYt" class="video-yt" src="/yt.png" alt="云台" @click="visibleYt">
</div>
<teleport to="body">
<Yt ref="ytRef" :title="ytState.title" :draggable="true" :resizable="false" />
</teleport>
</div>
</template>
@@ -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) {
+389
View File
@@ -0,0 +1,389 @@
<template>
<div class="yt_info_popup" v-if="dialogVisible"
:style="{ left: position.x + 'px', top: position.y + 'px', width: size.width + 'px', height: size.height + 'px' }">
<div class="yt_popup_header" @pointerdown="handleHeaderMouseDown"
:style="{ cursor: props.draggable ? (isDragging ? 'grabbing' : 'grab') : 'default' }">
<div class="yt_popup_header_left">
<h3>云台控制</h3>
<h3 v-if="props.title">-{{ props.title }}</h3>
</div>
<button @click.stop="handleClose" class="close_button" @pointerdown.stop @mousedown.stop>×</button>
</div>
<div class="yt-container">
<div id="pantilt"></div>
</div>
<div v-if="props.resizable" class="resize_handle resize_se" @pointerdown.stop="startResize('se', $event)"></div>
<div v-if="props.resizable" class="resize_handle resize_sw" @pointerdown.stop="startResize('sw', $event)"></div>
<div v-if="props.resizable" class="resize_handle resize_ne" @pointerdown.stop="startResize('ne', $event)"></div>
<div v-if="props.resizable" class="resize_handle resize_nw" @pointerdown.stop="startResize('nw', $event)"></div>
</div>
</template>
<script setup>
import { ref, defineExpose, onUnmounted, onMounted, onBeforeUnmount, defineProps, nextTick } from "vue";
import { ytApi } from "@/api/v2"
import "./lib/panTilt.css"
import PanTilt from './lib/panTilt';
const props = defineProps({
title: { type: String, default: "" },
draggable: { type: Boolean, default: true },
resizable: { type: Boolean, default: true }
})
let YT = null
const dialogVisible = ref(false)
const minSize = { width: 350, height: 290 }
const sizeRatio = minSize.width / minSize.height
const size = ref({ width: minSize.width, height: minSize.height })
const position = ref({ x: 100, y: 100 })
const isDragging = ref(false)
const dragStart = ref({ x: 0, y: 0 })
const initialPosition = ref({ x: 0, y: 0 })
const dragPointerId = ref(null)
const dragPointerEl = ref(null)
const isResizing = ref(false)
const resizeStart = ref({ x: 0, y: 0 })
const initialSize = ref({ width: minSize.width, height: minSize.height })
const resizeInitialPosition = ref({ x: 0, y: 0 })
const resizeDirection = ref("se")
const resizePointerId = ref(null)
const resizePointerEl = ref(null)
function resetLayout() {
size.value = { width: minSize.width, height: minSize.height }
position.value = {
x: Math.max(0, (window.innerWidth - size.value.width) / 2),
y: Math.max(0, (window.innerHeight - size.value.height) / 2)
}
}
function startDrag(event) {
isDragging.value = true
dragPointerId.value = event.pointerId ?? null
dragPointerEl.value = event.currentTarget ?? null
dragStart.value = {
x: event.clientX,
y: event.clientY
}
initialPosition.value = {
x: position.value.x,
y: position.value.y
}
try {
if (dragPointerEl.value && dragPointerId.value !== null) {
dragPointerEl.value.setPointerCapture(dragPointerId.value)
dragPointerEl.value.addEventListener("pointermove", onDrag, { passive: false })
dragPointerEl.value.addEventListener("pointerup", stopDrag, { passive: false })
dragPointerEl.value.addEventListener("pointercancel", stopDrag, { passive: false })
}
} catch (e) { }
event.preventDefault()
event.stopPropagation()
}
function handleHeaderMouseDown(event) {
if (!props.draggable) return
startDrag(event)
}
function onDrag(event) {
if (!isDragging.value) return
const deltaX = event.clientX - dragStart.value.x
const deltaY = event.clientY - dragStart.value.y
const newX = initialPosition.value.x + deltaX
const newY = initialPosition.value.y + deltaY
const maxX = window.innerWidth - size.value.width
const maxY = window.innerHeight - size.value.height
position.value = {
x: Math.max(0, Math.min(maxX, newX)),
y: Math.max(0, Math.min(maxY, newY))
}
event.preventDefault()
}
function stopDrag() {
isDragging.value = false
try {
if (dragPointerEl.value) {
dragPointerEl.value.removeEventListener("pointermove", onDrag)
dragPointerEl.value.removeEventListener("pointerup", stopDrag)
dragPointerEl.value.removeEventListener("pointercancel", stopDrag)
if (dragPointerId.value !== null) {
dragPointerEl.value.releasePointerCapture?.(dragPointerId.value)
}
}
} catch (e) { }
dragPointerId.value = null
dragPointerEl.value = null
}
function startResize(direction, event) {
if (!props.resizable) return
isResizing.value = true
resizeDirection.value = direction
resizePointerId.value = event.pointerId ?? null
resizePointerEl.value = event.currentTarget ?? null
resizeStart.value = {
x: event.clientX,
y: event.clientY
}
initialSize.value = {
width: size.value.width,
height: size.value.height
}
resizeInitialPosition.value = {
x: position.value.x,
y: position.value.y
}
try {
if (resizePointerEl.value && resizePointerId.value !== null) {
resizePointerEl.value.setPointerCapture(resizePointerId.value)
resizePointerEl.value.addEventListener("pointermove", onResize, { passive: false })
resizePointerEl.value.addEventListener("pointerup", stopResize, { passive: false })
resizePointerEl.value.addEventListener("pointercancel", stopResize, { passive: false })
}
} catch (e) { }
event.preventDefault()
event.stopPropagation()
}
function onResize(event) {
if (!isResizing.value) return
const deltaX = event.clientX - resizeStart.value.x
const deltaY = event.clientY - resizeStart.value.y
const hasWest = resizeDirection.value.includes('w')
const hasNorth = resizeDirection.value.includes('n')
const candidateWidth = hasWest ? initialSize.value.width - deltaX : initialSize.value.width + deltaX
const candidateHeight = hasNorth ? initialSize.value.height - deltaY : initialSize.value.height + deltaY
const maxWidthByHorizontal = hasWest ? resizeInitialPosition.value.x + initialSize.value.width : window.innerWidth - resizeInitialPosition.value.x
const maxHeightByVertical = hasNorth ? resizeInitialPosition.value.y + initialSize.value.height : window.innerHeight - resizeInitialPosition.value.y
const maxWidthAllowed = Math.min(maxWidthByHorizontal, maxHeightByVertical * sizeRatio)
let targetWidth = Math.max(candidateWidth, candidateHeight * sizeRatio)
targetWidth = Math.max(minSize.width, Math.min(maxWidthAllowed, targetWidth))
const targetHeight = targetWidth / sizeRatio
size.value = {
width: targetWidth,
height: targetHeight
}
position.value = {
x: hasWest ? Math.max(0, resizeInitialPosition.value.x + (initialSize.value.width - targetWidth)) : resizeInitialPosition.value.x,
y: hasNorth ? Math.max(0, resizeInitialPosition.value.y + (initialSize.value.height - targetHeight)) : resizeInitialPosition.value.y
}
event.preventDefault()
}
function stopResize() {
isResizing.value = false
try {
if (resizePointerEl.value) {
resizePointerEl.value.removeEventListener("pointermove", onResize)
resizePointerEl.value.removeEventListener("pointerup", stopResize)
resizePointerEl.value.removeEventListener("pointercancel", stopResize)
if (resizePointerId.value !== null) {
resizePointerEl.value.releasePointerCapture?.(resizePointerId.value)
}
}
} catch (e) { }
resizePointerId.value = null
resizePointerEl.value = null
}
function resetState() {
stopDrag()
stopResize()
resetLayout()
YT = null
}
async function open() {
resetLayout()
dialogVisible.value = true
await nextTick()
YT = new PanTilt({
el: 'pantilt',
setPtzDirection: ytApi.ytOperateDirect,
setPtzCamera: ytApi.ytOperateCamera,
controlSitPosition: ytApi.ytSitPosition,
})
}
function handleClose() {
dialogVisible.value = false
resetState()
}
onUnmounted(() => {
YT = null
})
/**
* 打开云台参数
* @typedef {Object} ShowYtOptions
* @property {string} id 通道 id
* @property {string} cameraType 1-枪机,2-球机
* @property {string} capability 摄像头能力集, 值为接口返回,枪机需要,球机支持所有能力可不传此参数。
*/
/**
* 打开云台参数
* @param {ShowYtOptions} items 回放参数数组
*/
async function showYt(channel) {
await open()
YT.setChannel(channel)
}
/**
* 打开云台参数
* @param {ShowYtOptions} items 回放参数数组
*/
async function setChannel(channel) {
if (!dialogVisible.value) return
await nextTick()
if (!YT) {
YT = new PanTilt({
el: 'pantilt',
setPtzDirection: ytApi.ytOperateDirect,
setPtzCamera: ytApi.ytOperateCamera,
controlSitPosition: ytApi.ytSitPosition,
})
}
YT.setChannel(channel)
}
defineExpose({
isVisible: () => dialogVisible.value,
setChannel,
showYt,
open: open,
close: handleClose
})
onMounted(() => {
resetLayout()
})
onBeforeUnmount(() => {
stopDrag()
stopResize()
})
</script>
<style lang="less" scoped>
.yt-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.yt-container :deep(#pantilt) {
width: 300px;
height: 300px;
display: flex;
justify-content: center;
align-items: center;
}
.yt_info_popup {
display: flex;
flex-direction: column;
position: fixed;
background: rgba(25, 30, 45, 0.85);
border: 1px solid #0a529a;
color: white;
padding: 15px 20px;
border-radius: 8px;
min-width: 360px;
min-height: 360px;
z-index: 1000;
pointer-events: all;
transition: none;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.5);
user-select: none;
}
.yt_popup_header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #444;
padding: 5px 0 10px 0;
margin-bottom: 10px;
user-select: none;
transition: background-color 0.2s ease;
}
.yt_popup_header_left {
display: flex;
justify-content: center;
align-items: center;
}
.yt_info_popup h3 {
margin-top: 0;
font-size: 18px;
border-bottom: none;
padding-bottom: 0;
margin-bottom: 0;
}
.yt_info_popup .close_button {
width: auto;
padding: 0 5px;
margin: 0;
border: none;
background: transparent;
color: #aaa;
font-size: 24px;
line-height: 1;
cursor: pointer;
transition: color 0.3s;
}
.yt_info_popup .close_button:hover {
background-color: transparent;
color: white;
}
.resize_handle {
position: absolute;
width: 14px;
height: 14px;
border-right: 2px solid rgba(255, 255, 255, 0.6);
border-bottom: 2px solid rgba(255, 255, 255, 0.6);
opacity: 0.8;
}
.resize_se {
right: 4px;
bottom: 4px;
cursor: nwse-resize;
}
.resize_sw {
left: 4px;
bottom: 4px;
cursor: nesw-resize;
transform: scaleX(-1);
}
.resize_ne {
right: 4px;
top: 4px;
cursor: nesw-resize;
transform: scaleY(-1);
}
.resize_nw {
left: 4px;
top: 4px;
cursor: nwse-resize;
transform: scale(-1);
}
</style>
+37
View File
@@ -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}分钟`;
}
+12 -1
View File
@@ -7,6 +7,9 @@
<el-form-item style="width: 250px;" label="车牌号" prop="carNum">
<el-input v-model="state.queryForm.carNum" placeholder="请输入车牌号" />
</el-form-item>
<el-form-item style="width: 250px;" label="是否军车" prop="isArmyCar">
<dict-select v-model="state.queryForm.isArmyCar" :options="yes_no_type" placeholder="请选择" />
</el-form-item>
<el-form-item style="width: 350px;" label="入场时间" prop="enterTimeList">
<el-date-picker v-model="state.queryForm.enterTimeList" value-format="YYYY-MM-DD" type="daterange" />
</el-form-item>
@@ -77,6 +80,13 @@
</template>
</el-table-column>
</el-table-column>
<el-table-column label="停留时间" min-width="120">
<template #default="{ row }">
<span v-if="row.enterTime && row.exitTime">{{ timeDiff(row.enterTime, row.exitTime) }}</span>
<span v-else class="color-danger">未出场</span>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
@@ -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()
+7
View File
@@ -56,6 +56,12 @@
<span class="color-danger" v-else>未出场</span>
</template>
</el-table-column>
<el-table-column label="停留时间" min-width="120">
<template #default="{ row }">
<span v-if="row.inTime && row.outTime">{{ timeDiff(row.inTime, row.outTime) }}</span>
<span v-else class="color-danger">未出场</span>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
v-bind="state.pagination" />
@@ -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()
+2 -1
View File
@@ -60,7 +60,8 @@
<div class="flex items-center gap-2">
<dict-tag v-if="row.carNumColor" :options="dh_car_num_color" :value="row.carNumColor" />
<el-tag v-else type="danger">未知</el-tag>
<span>{{ row.carNum }}</span>
<span v-if="row.carNum">{{ row.carNum }}</span>
<span v-else class="color-danger">未知</span>
</div>
</template>
</el-table-column>
+10 -4
View File
@@ -12,6 +12,12 @@
<el-form-item style="width: 250px;" label="开门类型" prop="openType">
<dict-select v-model="state.queryForm.openType" :options="dh_door_open_type" placeholder="请选择进开门类型" />
</el-form-item>
<el-form-item style="width: 250px;" label="人员类型" prop="personType">
<select-person-car-type areaType="person" v-model="state.queryForm.personType" />
</el-form-item>
<el-form-item style="width: 250px;" label="区域" prop="areaType">
<select-area-type areaType="person" v-model="state.queryForm.areaType" />
</el-form-item>
<el-form-item style="width: 450px;" label="抓拍时间" prop="swingTimeList">
<el-date-picker v-model="state.queryForm.swingTimeList" value-format="YYYY-MM-DD HH:mm:ss"
type="datetimerange" />
@@ -44,25 +50,25 @@
<el-table-column prop="personCode" min-width="100" label="人员编号">
<template #default="{ row }">
<span v-if="row.personCode">{{ row.personCode }}</span>
<span v-else class="color-danger">访客</span>
<span v-else class="color-danger">未知</span>
</template>
</el-table-column>
<el-table-column prop="personName" min-width="100" label="姓名">
<template #default="{ row }">
<span v-if="row.personName">{{ row.personName }}</span>
<span v-else class="color-danger">访客</span>
<span v-else class="color-danger">未知</span>
</template>
</el-table-column>
<el-table-column prop="paperNumber" min-width="100" label="证件号码" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.paperNumber">{{ row.paperNumber }}</span>
<span v-else class="color-danger">访客</span>
<span v-else class="color-danger">未知</span>
</template>
</el-table-column>
<el-table-column prop="deptName" min-width="100" label="部门名称">
<template #default="{ row }">
<span v-if="row.deptName">{{ row.deptName }}</span>
<span v-else class="color-danger">访客</span>
<span v-else class="color-danger">未知</span>
</template>
</el-table-column>
</el-table-column>
+4 -4
View File
@@ -67,28 +67,28 @@
:label="columnSetting['personCode'].label">
<template #default="{ row }">
<span v-if="row.personCode">{{ row.personCode }}</span>
<span v-else class="color-danger">访客</span>
<span v-else class="color-danger">未知</span>
</template>
</el-table-column>
<el-table-column v-if="columnSetting['personName'].visible" prop="personName" min-width="100"
:label="columnSetting['personName'].label">
<template #default="{ row }">
<span v-if="row.personName">{{ row.personName }}</span>
<span v-else class="color-danger">访客</span>
<span v-else class="color-danger">未知</span>
</template>
</el-table-column>
<el-table-column v-if="columnSetting['paperNumber'].visible" prop="paperNumber" min-width="100"
:label="columnSetting['paperNumber'].label" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.paperNumber">{{ row.paperNumber }}</span>
<span v-else class="color-danger">访客</span>
<span v-else class="color-danger">未知</span>
</template>
</el-table-column>
<el-table-column v-if="columnSetting['deptName'].visible" prop="deptName" min-width="100"
:label="columnSetting['deptName'].label">
<template #default="{ row }">
<span v-if="row.deptName">{{ row.deptName }}</span>
<span v-else class="color-danger">访客</span>
<span v-else class="color-danger">未知</span>
</template>
</el-table-column>
<!-- end 人员 end -->