拆分播放器组件和云台控制组件

This commit is contained in:
lin 2026-06-10 18:15:45 +08:00
parent b3c192a8a9
commit f5494c0b95
14 changed files with 1441 additions and 858 deletions

View File

@ -182,7 +182,7 @@ export function wiper([deviceId, channelDeviceId, command]) {
})
}
export function ptz([deviceId, channelId, command, horizonSpeed, verticalSpeed, zoomSpeed]) {
export function ptz({ deviceId, channelId, command, horizonSpeed, verticalSpeed, zoomSpeed }) {
return request({
method: 'get',
url: `/api/front-end/ptz/${deviceId}/${channelId}`,

View File

@ -0,0 +1,152 @@
<template>
<div class="player-tabs-wrapper" ref="playerWrapper">
<el-tabs v-if="playerCount > 1" v-model="activePlayer" type="card" :stretch="true" @tab-click="changePlayer">
<el-tab-pane label="Jessibuca" name="jessibuca">
<div v-if="activePlayer === 'jessibuca'" class="player-video-area">
<jessibucaPlayer
ref="jessibuca"
style="width: 100%; height: 100%;"
:video-url="videoUrl"
:has-audio="hasAudio"
:show-button="showButton"
fluent autoplay live
/>
</div>
</el-tab-pane>
<el-tab-pane label="WebRTC" name="webRTC">
<div v-if="activePlayer === 'webRTC'" class="player-video-area">
<rtc-player
ref="webRTC"
style="width: 100%; height: 100%;"
:video-url="videoUrl"
:has-audio="hasAudio"
fluent autoplay live
/>
</div>
</el-tab-pane>
<el-tab-pane label="h265web" name="h265web">
<div v-if="activePlayer === 'h265web'" class="player-video-area">
<h265web
style="width: 100%; height: 100%;"
ref="h265web"
:video-url="videoUrl"
:has-audio="hasAudio"
:show-button="showButton"
fluent autoplay live
/>
</div>
</el-tab-pane>
</el-tabs>
<div v-if="playerCount <= 1" class="player-video-area">
<jessibucaPlayer
v-if="player.jessibuca"
ref="jessibuca"
style="width: 100%; height: 100%;"
:video-url="videoUrl"
:has-audio="hasAudio"
:show-button="showButton"
fluent autoplay live
/>
<rtc-player
v-if="player.webRTC"
ref="webRTC"
style="width: 100%; height: 100%;"
:video-url="videoUrl"
:has-audio="hasAudio"
fluent autoplay live
/>
<h265web
v-if="player.h265web"
ref="h265web"
style="width: 100%; height: 100%;"
:video-url="videoUrl"
:has-audio="hasAudio"
:show-button="showButton"
fluent autoplay live
/>
</div>
</div>
</template>
<script>
import jessibucaPlayer from './jessibuca.vue'
import rtcPlayer from './rtcPlayer.vue'
import h265web from './h265web.vue'
export default {
name: 'PlayerTabs',
components: { jessibucaPlayer, rtcPlayer, h265web },
props: {
videoUrl: { type: String, default: '' },
hasAudio: { type: Boolean, default: false },
showButton: { type: Boolean, default: true },
height: { type: String, default: '' }
},
data() {
return {
activePlayer: 'jessibuca',
player: { jessibuca: ['ws_flv', 'wss_flv'], webRTC: ['rtc', 'rtcs'], h265web: ['ws_flv', 'wss_flv'] }
}
},
computed: {
playerCount() {
return Object.keys(this.player).length
}
},
created() {
if (this.playerCount === 1) {
this.activePlayer = Object.keys(this.player)[0]
}
},
methods: {
getUrlByStreamInfo(streamInfo) {
const info = streamInfo || this.streamInfo
if (!info) return ''
const src = info.transcodeStream || info
if (location.protocol === 'https:') {
return src[this.player[this.activePlayer][1]]
}
return src[this.player[this.activePlayer][0]]
},
changePlayer(tab) {
this.activePlayer = tab.name
this.$emit('player-changed', this.activePlayer)
},
play(url) {
this.$nextTick(() => {
if (this.$refs[this.activePlayer]) {
this.$refs[this.activePlayer].play(url)
}
})
},
stop() {
if (this.$refs[this.activePlayer]) {
this.$refs[this.activePlayer].pause()
}
},
pause() {
if (this.$refs[this.activePlayer]) {
this.$refs[this.activePlayer].pause()
}
}
}
}
</script>
<style scoped>
.player-tabs-wrapper {
width: 100%;
height: 100%;
}
.player-tabs-wrapper .el-tabs {
margin-bottom: 0;
}
.player-tabs-wrapper .el-tabs >>> .el-tabs__header {
margin-bottom: 0;
}
.player-video-area {
width: 100%;
height: 100%;
background: #000;
}
</style>

View File

@ -0,0 +1,233 @@
<template>
<div class="ptz-section-inner">
<div class="ptz-left">
<div class="ptz-dpad">
<div class="dpad-ring"></div>
<button class="dpad-btn card card-up" @mousedown.prevent="$emit('ptz-move', { direction: 'up', speed: controSpeed })" @mouseup.prevent="$emit('ptz-stop')"></button>
<button class="dpad-btn card card-right" @mousedown.prevent="$emit('ptz-move', { direction: 'right', speed: controSpeed })" @mouseup.prevent="$emit('ptz-stop')"></button>
<button class="dpad-btn card card-down" @mousedown.prevent="$emit('ptz-move', { direction: 'down', speed: controSpeed })" @mouseup.prevent="$emit('ptz-stop')"></button>
<button class="dpad-btn card card-left" @mousedown.prevent="$emit('ptz-move', { direction: 'left', speed: controSpeed })" @mouseup.prevent="$emit('ptz-stop')"></button>
<button class="dpad-btn diag diag-upright" @mousedown.prevent="$emit('ptz-move', { direction: 'upright', speed: controSpeed })" @mouseup.prevent="$emit('ptz-stop')"><span style="display:inline-block;transform:rotate(45deg)"></span></button>
<button class="dpad-btn diag diag-downright" @mousedown.prevent="$emit('ptz-move', { direction: 'downright', speed: controSpeed })" @mouseup.prevent="$emit('ptz-stop')"><span style="display:inline-block;transform:rotate(135deg)"></span></button>
<button class="dpad-btn diag diag-downleft" @mousedown.prevent="$emit('ptz-move', { direction: 'downleft', speed: controSpeed })" @mouseup.prevent="$emit('ptz-stop')"><span style="display:inline-block;transform:rotate(225deg)"></span></button>
<button class="dpad-btn diag diag-upleft" @mousedown.prevent="$emit('ptz-move', { direction: 'upleft', speed: controSpeed })" @mouseup.prevent="$emit('ptz-stop')"><span style="display:inline-block;transform:rotate(-45deg)"></span></button>
<button class="dpad-btn dpad-center" title="停止" @click="$emit('ptz-stop')"></button>
</div>
<div class="ptz-speed-slider">
<span class="ptz-speed-label">速度</span>
<el-slider v-model="controSpeed" :max="8" :min="1" style="flex: 1" />
</div>
</div>
<div class="ptz-right">
<div class="ptz-func-group">
<div class="ptz-func-row">
<div class="ptz-func-btn" title="变倍+" @mousedown.prevent="$emit('ptz-move', { direction: 'zoomin', speed: controSpeed })" @mouseup.prevent="$emit('ptz-stop')">
<i class="el-icon-zoom-in" /><span>变倍+</span>
</div>
<div class="ptz-func-btn" title="变倍-" @mousedown.prevent="$emit('ptz-move', { direction: 'zoomout', speed: controSpeed })" @mouseup.prevent="$emit('ptz-stop')">
<i class="el-icon-zoom-out" /><span>变倍-</span>
</div>
</div>
<div class="ptz-func-row">
<div class="ptz-func-btn" title="聚焦+" @mousedown.prevent="$emit('focus-move', { command: 'near', speed: controSpeed })" @mouseup.prevent="$emit('focus-stop')">
<i class="iconfont icon-bianjiao-fangda" /><span>聚焦+</span>
</div>
<div class="ptz-func-btn" title="聚焦-" @mousedown.prevent="$emit('focus-move', { command: 'far', speed: controSpeed })" @mouseup.prevent="$emit('focus-stop')">
<i class="iconfont icon-bianjiao-suoxiao" /><span>聚焦-</span>
</div>
</div>
<div class="ptz-func-row">
<div class="ptz-func-btn" title="光圈+" @mousedown.prevent="$emit('iris-move', { command: 'in', speed: controSpeed })" @mouseup.prevent="$emit('iris-stop')">
<i class="iconfont icon-guangquan" /><span>光圈+</span>
</div>
<div class="ptz-func-btn" title="光圈-" @mousedown.prevent="$emit('iris-move', { command: 'out', speed: controSpeed })" @mouseup.prevent="$emit('iris-stop')">
<i class="iconfont icon-guangquan-" /><span>光圈-</span>
</div>
</div>
</div>
<ptzPrecise v-if="showPrecise" :device-id="deviceId" :channel-device-id="channelId" @position="$emit('precise-position', $event)" style="margin-top: 6px" />
</div>
</div>
</template>
<script>
import ptzPrecise from './ptzPrecise.vue'
export default {
name: 'PtzControls',
components: { ptzPrecise },
props: {
deviceId: { type: String, default: null },
channelId: { type: String, default: null },
showPrecise: { type: Boolean, default: true }
},
data() {
return {
controSpeed: 5
}
},
mounted() {
window.addEventListener('mouseup', this.onWindowMouseUp)
},
beforeDestroy() {
window.removeEventListener('mouseup', this.onWindowMouseUp)
},
methods: {
onWindowMouseUp() {
this.$emit('ptz-stop')
}
}
}
</script>
<style scoped>
.ptz-section-inner {
display: flex;
padding: 8px 4px;
overflow-y: auto;
}
.ptz-left {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 12px;
}
.ptz-dpad {
position: relative;
width: 180px;
height: 180px;
flex: none;
}
.dpad-ring {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 130px;
height: 130px;
border-radius: 50%;
background: #f5f7fa;
pointer-events: none;
}
.dpad-btn {
position: absolute;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: transparent;
border: none;
outline: none;
padding: 0;
user-select: none;
transition: all 0.15s;
-webkit-tap-highlight-color: transparent;
}
.card {
width: 46px;
height: 46px;
font-size: 18px;
color: #303133;
}
.card:hover {
background: #409EFF;
color: #fff;
box-shadow: 0 3px 10px rgba(64,158,255,0.4);
transform: scale(1.1);
}
.card:active {
background: #337ecc;
transform: scale(0.92);
}
.card-up { top: 18px; left: 67px; }
.card-right { top: 67px; left: 116px; }
.card-down { top: 116px; left: 67px; }
.card-left { top: 67px; left: 18px; }
.diag {
width: 36px;
height: 36px;
font-size: 14px;
color: #a8abb2;
}
.diag:hover {
background: #409EFF;
color: #fff;
box-shadow: 0 2px 8px rgba(64,158,255,0.35);
transform: scale(1.1);
}
.diag:active {
background: #337ecc;
transform: scale(0.9);
}
.diag-upright { top: 40px; left: 110px; }
.diag-downright { top: 110px; left: 110px; }
.diag-downleft { top: 110px; left: 34px; }
.diag-upleft { top: 40px; left: 34px; }
.dpad-center {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
background: linear-gradient(135deg, #eef0f4, #e0e3e8);
font-size: 20px;
color: #909399;
line-height: 1;
}
.dpad-center:hover {
background: #409EFF;
color: #fff;
box-shadow: 0 3px 10px rgba(64,158,255,0.4);
transform: translate(-50%, -50%) scale(1.1);
}
.dpad-center:active {
background: #337ecc;
transform: translate(-50%, -50%) scale(0.92);
}
.ptz-speed-slider {
display: flex;
align-items: center;
width: 120px;
margin-top: 8px;
}
.ptz-speed-label {
font-size: 12px;
color: #606266;
margin-right: 6px;
white-space: nowrap;
}
.ptz-right { flex: 1; }
.ptz-func-group {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.ptz-func-row {
display: flex;
gap: 4px;
width: 100%;
}
.ptz-func-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 44px;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
background: #fff;
user-select: none;
font-size: 11px;
}
.ptz-func-btn:hover {
background: #409EFF;
color: #fff;
}
.ptz-func-btn:active {
background: #337ecc;
}
.ptz-func-btn i { font-size: 14px; margin-bottom: 2px; }
</style>

View File

@ -0,0 +1,65 @@
<template>
<div id="ptzPrecise">
<div class="precise-title">精确控制</div>
<div class="precise-row">
<span class="precise-label">水平</span>
<el-input-number size="mini" v-model="form.pan" :min="0" :max="3600" style="width: 100%" />
</div>
<div class="precise-row">
<span class="precise-label">垂直</span>
<el-input-number size="mini" v-model="form.tilt" :min="-1800" :max="1800" style="width: 100%" />
</div>
<div class="precise-row">
<span class="precise-label">变倍</span>
<el-input-number size="mini" v-model="form.zoom" :min="1" :max="128" style="width: 100%" />
</div>
<el-button size="mini" type="primary" style="width: 100%; margin-top: 4px" @click="emitPosition">定位</el-button>
</div>
</template>
<script>
export default {
name: 'PtzPrecise',
props: {
deviceId: { type: String, default: null },
channelDeviceId: { type: String, default: null }
},
data() {
return {
form: { pan: 0, tilt: 0, zoom: 1 }
}
},
methods: {
emitPosition() {
this.$emit('position', { ...this.form })
}
}
}
</script>
<style scoped>
#ptzPrecise {
padding: 6px 10px;
border: 1px solid #e4e7ed;
border-radius: 4px;
background: #fafafa;
}
.precise-title {
font-size: 12px;
color: #606266;
margin-bottom: 4px;
font-weight: 500;
}
.precise-row {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.precise-label {
font-size: 12px;
color: #909399;
width: 32px;
text-align: left;
flex-shrink: 0;
}
</style>

View File

@ -3,46 +3,22 @@
<el-tag
v-for="item in presetList"
:key="item.presetId"
closable
size="mini"
style="margin-right: 1rem; cursor: pointer; margin-bottom: 0.6rem"
@close="delPreset(item)"
@click="gotoPreset(item)"
>
{{ item.presetName?item.presetName:item.presetId }}
{{ item.presetName || item.presetId }}
</el-tag>
<el-input
v-if="inputVisible"
ref="saveTagInput"
v-model="ptzPresetId"
min="1"
max="255"
placeholder="预置位编号"
addon-before="预置位编号"
addon-after="(1-255)"
style="width: 300px; vertical-align: bottom;"
size="small"
>
<template v-slot:append>
<el-button @click="addPreset()">保存</el-button>
<el-button @click="cancel()">取消</el-button>
</template>
</el-input>
<el-button v-else size="small" @click="showInput">+ 添加</el-button>
</div>
</template>
<script>
export default {
name: 'PtzPreset',
components: {},
props: ['channelDeviceId', 'deviceId'],
data() {
return {
presetList: [],
inputVisible: false,
ptzPresetId: ''
presetList: []
}
},
created() {
@ -53,54 +29,11 @@ export default {
this.$store.dispatch('frontEnd/queryPreset', [this.deviceId, this.channelDeviceId])
.then(data => {
this.presetList = data
//
this.$nextTick(() => {
this.$refs.channelListTable.doLayout()
})
})
},
showInput() {
this.inputVisible = true
this.$nextTick(_ => {
this.$refs.saveTagInput.$refs.input.focus()
})
},
addPreset: function() {
const loading = this.$loading({
lock: true,
fullscreen: true,
text: '正在发送指令',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
})
this.$store.dispatch('frontEnd/addPreset', [this.deviceId, this.channelDeviceId, this.ptzPresetId])
.then(data => {
setTimeout(() => {
this.inputVisible = false
this.ptzPresetId = ''
this.getPresetList()
}, 1000)
}).catch((error) => {
loading.close()
this.inputVisible = false
this.ptzPresetId = ''
this.$message({
showClose: true,
message: error,
type: 'error'
})
}).finally(() => {
loading.close()
})
},
cancel: function() {
this.inputVisible = false
this.ptzPresetId = ''
},
gotoPreset: function(preset) {
console.log(preset)
this.$store.dispatch('frontEnd/callPreset', [this.deviceId, this.channelDeviceId, preset.presetId])
.then(data => {
.then(() => {
this.$message({
showClose: true,
message: '调用成功',
@ -113,40 +46,7 @@ export default {
type: 'error'
})
})
},
delPreset: function(preset) {
this.$confirm('确定删除此预置位', '提示', {
dangerouslyUseHTMLString: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const loading = this.$loading({
lock: true,
fullscreen: true,
text: '正在发送指令',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
})
this.$store.dispatch('frontEnd/deletePreset', [this.deviceId, this.channelDeviceId, preset.presetId])
.then(data => {
setTimeout(() => {
this.getPresetList()
}, 1000)
}).catch((error) => {
this.$message({
showClose: true,
message: error,
type: 'error'
})
}).finally(() => {
loading.close()
})
}).catch(() => {
})
}
}
}
</script>

View File

@ -1,6 +1,6 @@
<template>
<div id="channelList" style="height: calc(100vh - 124px);">
<div v-if="!editId" style="height: 100%">
<div v-if="!editId && !ptzConfigChannelDeviceId" style="height: 100%">
<el-form :inline="true" size="mini">
<el-form-item style="margin-right: 2rem">
<el-page-header content="通道列表" @back="showDevice" />
@ -189,6 +189,8 @@
设备录像控制-开始</el-dropdown-item>
<el-dropdown-item command="stopRecord" :disabled="device == null || device.online === 0">
设备录像控制-停止</el-dropdown-item>
<el-dropdown-item command="ptzConfig" :disabled="device == null || device.online === 0">
云台配置</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
@ -209,6 +211,7 @@
<devicePlayer ref="devicePlayer" />
<channel-edit v-if="editId" :id="editId" :close-edit="closeEdit" />
<ptzConfig v-if="ptzConfigChannelDeviceId" :device-id="ptzConfigDeviceId" :channel-device-id="ptzConfigChannelDeviceId" @close="closePtzConfig" />
</div>
</template>
@ -216,12 +219,14 @@
<script>
import devicePlayer from '../../dialog/devicePlayer.vue'
import Edit from './edit.vue'
import ptzConfig from '@/views/device/common/ptzConfig.vue'
export default {
name: 'ChannelList',
components: {
devicePlayer,
ChannelEdit: Edit
ChannelEdit: Edit,
ptzConfig
},
props: {
defaultPage: {
@ -258,6 +263,8 @@ export default {
total: 0,
beforeUrl: '/device',
editId: null,
ptzConfigDeviceId: null,
ptzConfigChannelDeviceId: null,
loadSnap: {},
ptzTypes: {
0: '未知',
@ -278,7 +285,6 @@ export default {
}
},
mounted() {
console.log(23222)
if (this.deviceId) {
this.$store.dispatch('device/queryDeviceOne', this.deviceId)
.then(data => {
@ -372,6 +378,10 @@ export default {
itemData.playLoading = false
})
},
closePtzConfig: function() {
this.ptzConfigDeviceId = null
this.ptzConfigChannelDeviceId = null
},
moreClick: function(command, itemData) {
if (command === 'records') {
this.queryRecords(itemData)
@ -381,6 +391,10 @@ export default {
this.startRecord(itemData)
} else if (command === 'stopRecord') {
this.stopRecord(itemData)
} else if (command === 'ptzConfig') {
console.log(itemData.channelId)
this.ptzConfigDeviceId = this.deviceId
this.ptzConfigChannelDeviceId = itemData.deviceId
}
},
queryRecords: function(itemData) {

View File

@ -0,0 +1,124 @@
<template>
<div class="player-ptz-panel">
<div class="player-section">
<div class="player-wrapper" :style="{ height: playerHeight }">
<playerTabs ref="playerTabs" :video-url="videoUrl" :has-audio="hasAudio" :show-button="true" />
</div>
</div>
<div class="ptz-section">
<ptzControls
:device-id="deviceId"
:channel-id="channelDeviceId"
:show-precise="false"
@ptz-move="onPtzMove"
@ptz-stop="onPtzStop"
@focus-move="onFocusMove"
@focus-stop="onFocusStop"
@iris-move="onIrisMove"
@iris-stop="onIrisStop"
/>
</div>
</div>
</template>
<script>
import playerTabs from '../../common/playerTabs.vue'
import ptzControls from '../../common/ptzControls.vue'
export default {
name: 'PlayerPtzPanel',
components: { playerTabs, ptzControls },
props: {
deviceId: { type: String, default: null },
channelDeviceId: { type: String, default: null }
},
data() {
return {
streamInfo: null,
videoUrl: '',
hasAudio: false,
playerHeight: '36vh'
}
},
mounted() {
this.startPlay()
},
beforeDestroy() {
this.stopPlay()
},
methods: {
ptzSpeed(speed) {
return parseInt(speed * 255 / 8)
},
onPtzMove(e) {
const speedVal = this.ptzSpeed(e.speed)
this.$store.dispatch('frontEnd/ptz', [this.deviceId, this.channelDeviceId, e.direction, speedVal, speedVal, speedVal])
},
onPtzStop() {
this.$store.dispatch('frontEnd/ptz', [this.deviceId, this.channelDeviceId, 'stop', 0, 0, 0])
},
onFocusMove(e) {
const speedVal = this.ptzSpeed(e.speed)
this.$store.dispatch('frontEnd/focus', [this.deviceId, this.channelDeviceId, e.command, speedVal])
},
onFocusStop() {
this.$store.dispatch('frontEnd/focus', [this.deviceId, this.channelDeviceId, 'stop', 0])
},
onIrisMove(e) {
const speedVal = this.ptzSpeed(e.speed)
this.$store.dispatch('frontEnd/iris', [this.deviceId, this.channelDeviceId, e.command, speedVal])
},
onIrisStop() {
this.$store.dispatch('frontEnd/iris', [this.deviceId, this.channelDeviceId, 'stop', 0])
},
startPlay() {
this.$store.dispatch('play/play', [this.deviceId, this.channelDeviceId])
.then(data => {
this.streamInfo = data
this.hasAudio = data.hasAudio
this.videoUrl = this.getUrlByStreamInfo(data)
this.$nextTick(() => {
if (this.$refs.playerTabs) {
this.$refs.playerTabs.play(this.videoUrl)
}
})
})
.catch(e => {
this.$message({ showClose: true, message: e || '播放失败', type: 'error' })
})
},
stopPlay() {
this.$store.dispatch('play/stop', { deviceId: this.deviceId, channelId: this.channelDeviceId })
.catch(() => {})
},
getUrlByStreamInfo(streamInfo) {
const info = streamInfo || this.streamInfo
if (!info) return ''
const src = info.transcodeStream || info
if (location.protocol === 'https:') {
return src['wss_flv']
}
return src['ws_flv']
}
}
}
</script>
<style scoped>
.player-ptz-panel {
display: flex;
flex-direction: column;
height: 100%;
}
.player-section {
flex: 1;
}
.ptz-section {
flex-shrink: 0;
display: flex;
}
.player-wrapper {
position: relative;
width: 100%;
}
</style>

View File

@ -0,0 +1,112 @@
<template>
<div id="dhPtzConfigPage">
<el-page-header content="云台设置" @back="$emit('close')" />
<div class="ptz-config-body">
<div class="config-sidebar">
<el-menu :default-active="activeTab" @select="handleMenuSelect">
<el-menu-item index="preset">
<i class="el-icon-map-location" style="margin-right: 6px" />
<span>预置点</span>
</el-menu-item>
<el-menu-item index="cruise">
<i class="el-icon-s-order" style="margin-right: 6px" />
<span>巡航组</span>
</el-menu-item>
<el-menu-item index="scan">
<i class="el-icon-s-grid" style="margin-right: 6px" />
<span>自动扫描</span>
</el-menu-item>
<el-menu-item index="wiper">
<i class="el-icon-umbrella" style="margin-right: 6px" />
<span>雨刷</span>
</el-menu-item>
<el-menu-item index="switch">
<i class="el-icon-s-tools" style="margin-right: 6px" />
<span>辅助开关</span>
</el-menu-item>
</el-menu>
</div>
<div class="content-wrapper">
<div class="player-panel">
<playerPtzPanel :device-id="deviceId" :channel-device-id="channelDeviceId" />
</div>
<div class="tab-panel">
<ptzPresetConfig v-if="activeTab === 'preset'" :device-id="deviceId" :channel-device-id="channelDeviceId" />
<ptzCruiseConfig v-if="activeTab === 'cruise'" :device-id="deviceId" :channel-device-id="channelDeviceId" />
<ptzScanConfig v-if="activeTab === 'scan'" :device-id="deviceId" :channel-device-id="channelDeviceId" />
<ptzWiperConfig v-if="activeTab === 'wiper'" :device-id="deviceId" :channel-device-id="channelDeviceId" />
<ptzSwitchConfig v-if="activeTab === 'switch'" :device-id="deviceId" :channel-device-id="channelDeviceId" />
</div>
</div>
</div>
</div>
</template>
<script>
import playerPtzPanel from './playerPtzPanel.vue'
import ptzPresetConfig from './ptzPresetConfig.vue'
import ptzCruiseConfig from './ptzCruiseConfig.vue'
import ptzScanConfig from './ptzScanConfig.vue'
import ptzWiperConfig from './ptzWiperConfig.vue'
import ptzSwitchConfig from './ptzSwitchConfig.vue'
export default {
name: 'PtzConfigPage',
components: { playerPtzPanel, ptzPresetConfig, ptzCruiseConfig, ptzScanConfig, ptzWiperConfig, ptzSwitchConfig },
props: {
deviceId: { type: String, default: null },
channelDeviceId: { type: String, default: null }
},
data() {
return {
activeTab: 'preset'
}
},
methods: {
handleMenuSelect(index) {
this.activeTab = index
}
}
}
</script>
<style scoped>
#dhPtzConfigPage {
height: 100%;
display: flex;
flex-direction: column;
}
.ptz-config-body {
flex: 1;
display: flex;
overflow: hidden;
padding-top: 16px;
}
.config-sidebar {
width: 140px;
flex: none;
border-right: 1px solid #e6e6e6;
overflow-y: auto;
}
.config-sidebar .el-menu {
border-right: none;
}
.content-wrapper {
flex: 1;
display: flex;
overflow: hidden;
}
.player-panel {
width: 600px;
flex: none;
display: flex;
flex-direction: column;
border-right: 1px solid #e6e6e6;
padding: 0 12px;
}
.tab-panel {
flex: 1;
overflow: auto;
padding: 0 12px;
}
</style>

View File

@ -0,0 +1,200 @@
<template>
<div style="height: 100%; display: flex; flex-direction: column;">
<el-form size="small" inline style="margin-bottom: 12px; padding: 16px 8px; border: 1px solid #e6e6e6; border-radius: 4px;">
<el-form-item label="巡航组号" style="margin-bottom: 0;">
<el-input-number v-model="cruiseId" :min="1" :max="255" controls-position="right" style="width: 140px" />
</el-form-item>
</el-form>
<div v-if="presetPoints.length > 0" style="margin-bottom: 8px;">
<el-tag
v-for="(item, index) in presetPoints"
:key="index"
closable
size="small"
style="margin-right: 8px; margin-bottom: 4px;"
@close="removePoint(item, index)"
>
{{ item.presetName || ('预置点' + item.presetId) }}
</el-tag>
</div>
<div v-else style="color: #909399; font-size: 12px; margin-bottom: 8px;">暂无巡航点</div>
<el-form v-if="showAddPoint" size="mini" inline style="margin-bottom: 8px;">
<el-form-item style="margin-bottom: 0;">
<el-select v-model="selectedPreset" placeholder="选择预置点" style="width: 160px">
<el-option
v-for="p in allPresetList"
:key="p.presetId"
:label="p.presetName || ('预置点' + p.presetId)"
:value="p"
/>
</el-select>
</el-form-item>
<el-form-item style="margin-bottom: 0;">
<el-button type="primary" @click="addPoint">确定</el-button>
<el-button @click="showAddPoint = false">取消</el-button>
</el-form-item>
</el-form>
<el-button v-else size="small" style="margin-bottom: 8px;" @click="showAddPoint = true">添加巡航点</el-button>
<el-form v-if="showSpeedInput" size="mini" inline style="margin-bottom: 8px;">
<el-form-item label="速度" style="margin-bottom: 0;">
<el-input-number v-model="cruiseSpeed" :min="1" :max="255" controls-position="right" style="width: 120px" />
</el-form-item>
<el-form-item style="margin-bottom: 0;">
<el-button type="primary" @click="setSpeed">确定</el-button>
<el-button @click="cancelSpeed">取消</el-button>
</el-form-item>
</el-form>
<el-button v-else size="small" style="margin-bottom: 8px;" @click="openSpeed">设置巡航速度</el-button>
<el-form v-if="showTimeInput" size="mini" inline style="margin-bottom: 8px;">
<el-form-item label="停留时间(秒)" style="margin-bottom: 0;">
<el-input-number v-model="cruiseTime" :min="1" :max="300" controls-position="right" style="width: 120px" />
</el-form-item>
<el-form-item style="margin-bottom: 0;">
<el-button type="primary" @click="setTime">确定</el-button>
<el-button @click="cancelTime">取消</el-button>
</el-form-item>
</el-form>
<el-button v-else size="small" style="margin-bottom: 8px;" @click="openTime">设置巡航时间</el-button>
<div style="margin-top: 8px;">
<el-button size="small" type="primary" :loading="starting" :disabled="starting" @click="startCruise">开始巡航</el-button>
<el-button size="small" :loading="stopping" :disabled="stopping" @click="stopCruise">停止巡航</el-button>
<el-button size="small" type="danger" :loading="deleting" :disabled="deleting" @click="deleteCruise">删除巡航</el-button>
</div>
</div>
</template>
<script>
export default {
name: 'PtzCruiseConfig',
props: {
deviceId: { type: String, default: null },
channelDeviceId: { type: String, default: null }
},
data() {
return {
cruiseId: 1,
presetPoints: [],
allPresetList: [],
selectedPreset: null,
showAddPoint: false,
showSpeedInput: false,
showTimeInput: false,
cruiseSpeed: 5,
cruiseTime: 15,
starting: false,
stopping: false,
deleting: false
}
},
created() {
this.loadPresets()
},
methods: {
loadPresets() {
this.$store.dispatch('frontEnd/queryPreset', [this.deviceId, this.channelDeviceId])
.then(data => {
this.allPresetList = data || []
})
.catch(error => {
console.log('[巡航] 加载预置点列表失败', error)
})
},
addPoint() {
if (!this.selectedPreset) {
this.$message({ showClose: true, message: '请选择预置点', type: 'warning' })
return
}
this.$store.dispatch('frontEnd/addPointForCruise', [this.deviceId, this.channelDeviceId, this.cruiseId, this.selectedPreset.presetId])
.then(() => {
this.presetPoints.push(this.selectedPreset)
this.selectedPreset = null
this.showAddPoint = false
this.$message({ showClose: true, message: '添加成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
})
},
removePoint(preset, index) {
this.$store.dispatch('frontEnd/deletePointForCruise', [this.deviceId, this.channelDeviceId, this.cruiseId, preset.presetId])
.then(() => {
this.presetPoints.splice(index, 1)
this.$message({ showClose: true, message: '删除成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
})
},
openSpeed() {
this.showSpeedInput = true
},
cancelSpeed() {
this.showSpeedInput = false
this.cruiseSpeed = 5
},
setSpeed() {
this.$store.dispatch('frontEnd/setCruiseSpeed', [this.deviceId, this.channelDeviceId, this.cruiseId, this.cruiseSpeed])
.then(() => {
this.showSpeedInput = false
this.$message({ showClose: true, message: '速度设置成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
})
},
openTime() {
this.showTimeInput = true
},
cancelTime() {
this.showTimeInput = false
this.cruiseTime = 15
},
setTime() {
this.$store.dispatch('frontEnd/setCruiseTime', [this.deviceId, this.channelDeviceId, this.cruiseId, this.cruiseTime])
.then(() => {
this.showTimeInput = false
this.$message({ showClose: true, message: '时间设置成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
})
},
startCruise() {
this.starting = true
this.$store.dispatch('frontEnd/startCruise', [this.deviceId, this.channelDeviceId, this.cruiseId])
.then(() => {
this.$message({ showClose: true, message: '巡航启动成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
}).finally(() => {
this.starting = false
})
},
stopCruise() {
this.stopping = true
this.$store.dispatch('frontEnd/stopCruise', [this.deviceId, this.channelDeviceId, this.cruiseId])
.then(() => {
this.$message({ showClose: true, message: '巡航停止成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
}).finally(() => {
this.stopping = false
})
},
deleteCruise() {
this.$confirm('确定删除此巡航组所有点?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deleting = true
this.$store.dispatch('frontEnd/deletePointForCruise', [this.deviceId, this.channelDeviceId, this.cruiseId, 0])
.then(() => {
this.presetPoints = []
this.$message({ showClose: true, message: '删除成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
}).finally(() => {
this.deleting = false
})
}).catch(() => {})
}
}
}
</script>

View File

@ -0,0 +1,156 @@
<template>
<div style="height: 100%; display: flex; flex-direction: column;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;">
<div>
<el-button type="primary" :disabled="showAddForm" @click="openAdd">添加预置点</el-button>
<el-button :loading="clearing" :disabled="clearing" @click="clearAll">清空</el-button>
</div>
<el-button icon="el-icon-refresh-right" circle @click="getPresetList" />
</div>
<el-form v-if="showAddForm" size="small" inline style="margin-bottom: 6px; padding: 16px 8px; border: 1px solid #e6e6e6; border-radius: 4px; display: flex; align-items: center;">
<el-form-item label="序号" style="margin-bottom: 0; margin-right: 2rem">
<el-input-number v-model="addPresetId" :min="1" :max="255" controls-position="right" style="width: 180px" />
</el-form-item>
<el-form-item style="margin-bottom: 0;">
<el-button type="primary" :loading="submitting" :disabled="submitting" @click="confirmAdd">确定</el-button>
<el-button @click="cancelAdd">取消</el-button>
</el-form-item>
</el-form>
<el-table
:data="presetList"
border
stripe
max-height="100%"
style="flex: 1"
>
<el-table-column prop="presetId" label="序号" align="center" />
<el-table-column label="名称">
<template v-slot="{ row }">
<span>{{ row.presetName || ('预置点' + row.presetId) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" min-width="140" align="center">
<template v-slot="{ row }">
<el-button size="mini" type="text" @click="callPreset(row)">调用</el-button>
<el-button size="mini" type="text" style="color: #F56C6C" :loading="deletingId === row.presetId" :disabled="deletingId !== null" @click="delPreset(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
name: 'PtzPresetConfig',
props: {
deviceId: { type: String, default: null },
channelDeviceId: { type: String, default: null }
},
data() {
return {
presetList: [],
showAddForm: false,
addPresetId: 1,
submitting: false,
clearing: false,
deletingId: null
}
},
created() {
this.getPresetList()
},
methods: {
getPresetList() {
this.$store.dispatch('frontEnd/queryPreset', [this.deviceId, this.channelDeviceId])
.then(data => {
this.presetList = data || []
})
.catch(error => {
console.log(error)
})
},
openAdd() {
this.addPresetId = this.getNextAvailableId()
this.showAddForm = true
},
cancelAdd() {
this.showAddForm = false
this.addPresetId = 1
},
confirmAdd() {
const exists = this.presetList.some(p => p.presetId === this.addPresetId)
if (exists) {
this.$message({ showClose: true, message: '序号 ' + this.addPresetId + ' 已存在', type: 'warning' })
return
}
this.submitting = true
this.$store.dispatch('frontEnd/addPreset', [this.deviceId, this.channelDeviceId, this.addPresetId])
.then(() => {
this.showAddForm = false
setTimeout(() => {
this.getPresetList()
}, 600)
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
}).finally(() => {
this.submitting = false
})
},
callPreset(preset) {
this.$store.dispatch('frontEnd/callPreset', [this.deviceId, this.channelDeviceId, preset.presetId])
.then(() => {
this.$message({ showClose: true, message: '调用成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
})
},
delPreset(preset) {
this.$confirm('确定删除此预置位', '提示', {
dangerouslyUseHTMLString: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deletingId = preset.presetId
this.$store.dispatch('frontEnd/deletePreset', [this.deviceId, this.channelDeviceId, preset.presetId])
.then(() => {
this.getPresetList()
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
}).finally(() => {
this.deletingId = null
})
}).catch(() => {})
},
clearAll() {
if (this.presetList.length === 0) return
this.$confirm('确定清空所有预置点?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.clearing = true
const promises = this.presetList.map(p =>
this.$store.dispatch('frontEnd/deletePreset', [this.deviceId, this.channelDeviceId, p.presetId])
)
Promise.all(promises).then(() => {
this.presetList = []
this.$message({ showClose: true, message: '清空成功', type: 'success' })
}).catch(() => {
this.$message({ showClose: true, message: '清空失败', type: 'error' })
}).finally(() => {
this.clearing = false
})
}).catch(() => {})
},
getNextAvailableId() {
if (!this.presetList || this.presetList.length === 0) return 1
const used = this.presetList.map(p => p.presetId).sort((a, b) => a - b)
for (let i = 0; i < used.length - 1; i++) {
if (used[i + 1] - used[i] > 1) return used[i] + 1
}
return used[used.length - 1] + 1
}
}
}
</script>

View File

@ -0,0 +1,107 @@
<template>
<div style="height: 100%; display: flex; flex-direction: column;">
<el-form size="small" inline style="margin-bottom: 12px; padding: 16px 8px; border: 1px solid #e6e6e6; border-radius: 4px;">
<el-form-item label="扫描组号" style="margin-bottom: 0;">
<el-input-number v-model="scanId" :min="1" :max="255" controls-position="right" style="width: 140px" />
</el-form-item>
</el-form>
<div style="margin-bottom: 8px;">
<el-button size="small" :loading="leftLoading" :disabled="leftLoading" @click="setLeft">设置左边界</el-button>
<el-button size="small" :loading="rightLoading" :disabled="rightLoading" @click="setRight">设置右边界</el-button>
</div>
<el-form v-if="showSpeedInput" size="mini" inline style="margin-bottom: 8px;">
<el-form-item label="扫描速度" style="margin-bottom: 0;">
<el-input-number v-model="scanSpeed" :min="1" :max="255" controls-position="right" style="width: 120px" />
</el-form-item>
<el-form-item style="margin-bottom: 0;">
<el-button type="primary" @click="setSpeed">确定</el-button>
<el-button @click="cancelSpeed">取消</el-button>
</el-form-item>
</el-form>
<el-button v-else size="small" style="margin-bottom: 8px;" @click="showSpeedInput = true">设置扫描速度</el-button>
<div style="margin-top: 8px;">
<el-button size="small" type="primary" :loading="starting" :disabled="starting" @click="startScan">开始自动扫描</el-button>
<el-button size="small" :loading="stopping" :disabled="stopping" @click="stopScan">停止自动扫描</el-button>
</div>
</div>
</template>
<script>
export default {
name: 'PtzScanConfig',
props: {
deviceId: { type: String, default: null },
channelDeviceId: { type: String, default: null }
},
data() {
return {
scanId: 1,
showSpeedInput: false,
scanSpeed: 5,
leftLoading: false,
rightLoading: false,
starting: false,
stopping: false
}
},
methods: {
setLeft() {
this.leftLoading = true
this.$store.dispatch('frontEnd/setLeftForScan', [this.deviceId, this.channelDeviceId, this.scanId])
.then(() => {
this.$message({ showClose: true, message: '左边界设置成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
}).finally(() => {
this.leftLoading = false
})
},
setRight() {
this.rightLoading = true
this.$store.dispatch('frontEnd/setRightForScan', [this.deviceId, this.channelDeviceId, this.scanId])
.then(() => {
this.$message({ showClose: true, message: '右边界设置成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
}).finally(() => {
this.rightLoading = false
})
},
setSpeed() {
this.$store.dispatch('frontEnd/setSpeedForScan', [this.deviceId, this.channelDeviceId, this.scanId, this.scanSpeed])
.then(() => {
this.showSpeedInput = false
this.$message({ showClose: true, message: '速度设置成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
})
},
cancelSpeed() {
this.showSpeedInput = false
this.scanSpeed = 5
},
startScan() {
this.starting = true
this.$store.dispatch('frontEnd/startScan', [this.deviceId, this.channelDeviceId, this.scanId])
.then(() => {
this.$message({ showClose: true, message: '扫描启动成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
}).finally(() => {
this.starting = false
})
},
stopScan() {
this.stopping = true
this.$store.dispatch('frontEnd/stopScan', [this.deviceId, this.channelDeviceId, this.scanId])
.then(() => {
this.$message({ showClose: true, message: '扫描停止成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
}).finally(() => {
this.stopping = false
})
}
}
}
</script>

View File

@ -0,0 +1,42 @@
<template>
<div>
<el-form size="mini" inline>
<el-form-item label="开关编号" style="margin-bottom: 0;">
<el-input-number v-model="switchId" :min="1" :max="255" controls-position="right" style="width: 140px" />
</el-form-item>
<el-form-item style="margin-bottom: 0;">
<el-button size="small" :loading="loading" :disabled="loading" @click="control('on')">开启</el-button>
<el-button size="small" :loading="loading" :disabled="loading" @click="control('off')">关闭</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: 'PtzSwitchConfig',
props: {
deviceId: { type: String, default: null },
channelDeviceId: { type: String, default: null }
},
data() {
return {
switchId: 1,
loading: false
}
},
methods: {
control(command) {
this.loading = true
this.$store.dispatch('frontEnd/auxiliary', [this.deviceId, this.channelDeviceId, command, this.switchId])
.then(() => {
this.$message({ showClose: true, message: command === 'on' ? '开关已开启' : '开关已关闭', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
}).finally(() => {
this.loading = false
})
}
}
}
</script>

View File

@ -0,0 +1,34 @@
<template>
<div>
<el-button size="small" :loading="loading" :disabled="loading" @click="control('on')">开启</el-button>
<el-button size="small" :loading="loading" :disabled="loading" @click="control('off')">关闭</el-button>
</div>
</template>
<script>
export default {
name: 'PtzWiperConfig',
props: {
deviceId: { type: String, default: null },
channelDeviceId: { type: String, default: null }
},
data() {
return {
loading: false
}
},
methods: {
control(command) {
this.loading = true
this.$store.dispatch('frontEnd/wiper', [this.deviceId, this.channelDeviceId, command])
.then(() => {
this.$message({ showClose: true, message: command === 'on' ? '雨刷已开启' : '雨刷已关闭', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
}).finally(() => {
this.loading = false
})
}
}
}
</script>

File diff suppressed because it is too large Load Diff