云台配置 优化巡航组配置

This commit is contained in:
lin 2026-06-11 22:30:27 +08:00
parent fb6c84de29
commit 6931a95ecf
6 changed files with 287 additions and 184 deletions

View File

@ -1,6 +1,6 @@
@font-face {
font-family: "iconfont"; /* Project id 1291092 */
src: url('iconfont.woff2?t=1769409737891') format('woff2')
src: url('iconfont.woff2?t=1780559263294') format('woff2');
}
.iconfont {
@ -11,6 +11,54 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-dingshirenwuguanli:before {
content: "\e800";
}
.icon-zhongqi:before {
content: "\e801";
}
.icon-yunfuyangjiaojiao:before {
content: "\e7ff";
}
.icon-free:before {
content: "\e7fd";
}
.icon-Union-32:before {
content: "\e7fe";
}
.icon-ruanxianwei:before {
content: "\e7fc";
}
.icon-sudu:before {
content: "\e7fb";
}
.icon-shuipingxuanzhuan:before {
content: "\e7fa";
}
.icon-cengdie:before {
content: "\e7f9";
}
.icon-yinpin:before {
content: "\e7f6";
}
.icon-xiangjishezhi2:before {
content: "\e7f7";
}
.icon-cengdie3:before {
content: "\e815";
}
.icon-xintiao:before {
content: "\e7f4";
}

Binary file not shown.

View File

@ -223,7 +223,7 @@
import devicePlayer from '../../dialog/devicePlayer.vue'
import audioTalk from '../../dialog/audioTalk.vue'
import Edit from './edit.vue'
import ptzConfig from '@/views/device/common/ptzConfig.vue'
import ptzConfig from '@/views/device/channel/ptzConfig.vue'
export default {
name: 'ChannelList',

View File

@ -13,8 +13,8 @@
<span>巡航组</span>
</el-menu-item>
<el-menu-item index="scan">
<i class="el-icon-s-grid" style="margin-right: 6px" />
<span>自动扫描</span>
<i class="iconfont icon-slider-right" style="margin-right: 6px" />
<span>线性扫描</span>
</el-menu-item>
<el-menu-item index="wiper">
<i class="el-icon-umbrella" style="margin-right: 6px" />
@ -43,12 +43,12 @@
</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'
import playerPtzPanel from '../common/playerPtzPanel.vue'
import ptzPresetConfig from '../common/ptzPresetConfig.vue'
import ptzCruiseConfig from '../common/ptzCruiseConfig.vue'
import ptzScanConfig from '../common/ptzScanConfig.vue'
import ptzWiperConfig from '../common/ptzWiperConfig.vue'
import ptzSwitchConfig from '../common/ptzSwitchConfig.vue'
export default {
name: 'PtzConfigPage',

View File

@ -2,7 +2,7 @@
<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" />
<playerTabs ref="playerTabs" :has-audio="hasAudio" :show-button="true" />
</div>
</div>
<div class="ptz-section">
@ -34,8 +34,6 @@ export default {
},
data() {
return {
streamInfo: null,
videoUrl: '',
hasAudio: false,
playerHeight: '36vh'
}
@ -58,7 +56,7 @@ export default {
command: e.direction,
horizonSpeed: speedVal,
verticalSpeed: speedVal,
zoomSpeed: speedVal
zoomSpeed: parseInt(e.speed * 15 / 100)
})
},
onPtzStop() {
@ -88,12 +86,10 @@ export default {
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)
this.$refs.playerTabs.setStreamInfo(data)
}
})
})
@ -105,15 +101,7 @@ export default {
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>
@ -125,7 +113,7 @@ export default {
height: 100%;
}
.player-section {
flex: 1;
flex: 0.8;
}
.ptz-section {
flex-shrink: 0;

View File

@ -1,65 +1,76 @@
<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 id="ptzCruiseConfig" 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="formVisible" @click="openAdd">添加巡航组</el-button>
<el-button :loading="clearing" :disabled="clearing" @click="clearCruiseTours">清空</el-button>
</div>
<el-button icon="el-icon-refresh-right" circle @click="loadPresets" />
</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 v-if="formVisible" style="margin-bottom: 6px; padding: 16px 8px; border: 1px solid #e6e6e6; border-radius: 4px;">
<el-form inline size="small" style="display: flex; align-items: center; margin-top: 15px;">
<el-form-item label="序号" style="margin-bottom: 0;">
<el-input-number v-model="formId" :min="0" :max="255" controls-position="right" style="width: 120px" />
</el-form-item>
<el-form-item label="名称" style="margin-bottom: 0;">
<el-input v-model="formName" placeholder="名称" style="width: 140px" />
</el-form-item>
<el-form-item style="margin-bottom: 0;">
<el-button type="primary" :loading="submitting" :disabled="submitting" @click="confirmSave">确定</el-button>
<el-button @click="cancelForm">取消</el-button>
</el-form-item>
</el-form>
<el-divider style="margin: 6px 0;" />
<div style="margin-bottom: 4px;">
<el-button size="mini" type="primary" @click="addPresetRow">添加预置点</el-button>
</div>
<el-table :data="formPresets" size="mini" stripe border max-height="200px">
<el-table-column label="序号" width="50">
<template v-slot="{ $index }">{{ $index + 1 }}</template>
</el-table-column>
<el-table-column label="预置点" min-width="100">
<template v-slot="{ row }">
<el-select v-model="row.presetId" size="mini" style="width: 120px" placeholder="选择预置点">
<el-option v-for="p in allPresetList" :key="p.presetId"
:label="p.presetName || ('预置点' + p.presetId)"
:value="p.presetId" />
</el-select>
</template>
</el-table-column>
<el-table-column label="停留时间(秒)" min-width="100">
<template v-slot="{ row }">
<el-input-number v-model="row.dwellTime" :min="15" :max="300" size="mini" controls-position="right" style="width: 90px" />
</template>
</el-table-column>
<el-table-column label="速度" min-width="100">
<template v-slot="{ row }">
<el-select v-model="row.speed" size="mini" style="width: 90px">
<el-option v-for="s in 10" :key="s" :label="s" :value="s" />
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" width="60">
<template v-slot="{ $index }">
<el-button size="mini" type="text" style="color: #F56C6C" @click="removePresetRow($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-if="cruiseTours.length > 0" style="flex: 1; overflow: auto;">
<el-table ref="cruiseTable" :data="cruiseTours" size="mini" max-height="100%" stripe border highlight-current-row>
<el-table-column prop="id" label="ID" />
<el-table-column prop="name" label="巡航名称" />
<el-table-column label="操作" min-width="150">
<template v-slot:default="scope">
<el-button v-if="cruisingCruiseId === scope.row.id" size="mini" type="text" style="color: #F56C6C" :loading="operatingId === scope.row.id" :disabled="operatingId !== null" @click="stopCruise(scope.row)">停用</el-button>
<el-button v-else size="mini" type="text" :disabled="cruisingCruiseId !== null || operatingId !== null" style="color: #409EFF" :loading="operatingId === scope.row.id" @click="startCruise(scope.row)">启用</el-button>
<el-button size="mini" type="text" style="color: #409EFF" :disabled="operatingId !== null" @click="openEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="text" style="color: #F56C6C" :loading="deletingId === scope.row.id" :disabled="operatingId !== null || deletingId !== null" @click="deleteCruise(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else style="color: #909399; font-size: 12px; margin-bottom: 8px;">暂无巡航路线</div>
</div>
</template>
@ -72,18 +83,18 @@ export default {
},
data() {
return {
cruiseId: 1,
presetPoints: [],
allPresetList: [],
selectedPreset: null,
showAddPoint: false,
showSpeedInput: false,
showTimeInput: false,
cruiseSpeed: 5,
cruiseTime: 15,
starting: false,
stopping: false,
deleting: false
cruiseTours: [],
cruisingCruiseId: null,
formVisible: false,
editingTourId: null,
submitting: false,
clearing: false,
operatingId: null,
deletingId: null,
formId: 1,
formName: '',
formPresets: [],
allPresetList: []
}
},
created() {
@ -99,99 +110,155 @@ export default {
console.log('[巡航] 加载预置点列表失败', error)
})
},
addPoint() {
if (!this.selectedPreset) {
this.$message({ showClose: true, message: '请选择预置点', type: 'warning' })
getNextAvailableId() {
const used = new Set((this.cruiseTours || []).map(t => t.id))
for (let i = 0; i <= 255; i++) {
if (!used.has(i)) return i
}
return 0
},
openAdd() {
this.editingTourId = null
this.formId = this.getNextAvailableId()
this.formName = '巡航组' + this.formId
this.formPresets = []
this.formVisible = true
},
openEdit(tour) {
this.editingTourId = tour.id
this.formId = tour.id
this.formName = tour.name
this.formPresets = (tour.presets || []).map(p => ({
presetId: p.presetId,
dwellTime: p.dwellTime,
speed: p.speed
}))
if (this.formPresets.length === 0) {
this.formPresets.push({ presetId: this.getFirstPresetId(), dwellTime: 15, speed: 7 })
}
this.formVisible = true
},
cancelForm() {
this.formVisible = false
this.editingTourId = null
this.formPresets = []
},
getFirstPresetId() {
const first = this.allPresetList[0]
return first ? first.presetId : 1
},
addPresetRow() {
this.formPresets.push({
presetId: this.getFirstPresetId(),
dwellTime: 15,
speed: 7
})
},
removePresetRow(index) {
this.formPresets.splice(index, 1)
},
confirmSave() {
if (!this.formName.trim()) {
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' })
})
if (this.formId == null || this.formId < 0 || this.formId > 255) {
this.$message({ showClose: true, message: '巡航序号必须在0-255之间', type: 'warning' })
return
}
this.submitting = true
let chain = Promise.resolve()
if (this.editingTourId !== null) {
chain = chain.then(() => this.$store.dispatch('frontEnd/deletePointForCruise', [this.deviceId, this.channelDeviceId, this.formId, 0]))
}
this.formPresets.forEach(p => {
chain = chain.then(() => this.$store.dispatch('frontEnd/addPointForCruise', [this.deviceId, this.channelDeviceId, this.formId, p.presetId]))
})
const speed = this.formPresets.length > 0 ? this.formPresets[0].speed : 7
const dwellTime = this.formPresets.length > 0 ? this.formPresets[0].dwellTime : 15
chain = chain.then(() => this.$store.dispatch('frontEnd/setCruiseSpeed', [this.deviceId, this.channelDeviceId, this.formId, speed]))
chain = chain.then(() => this.$store.dispatch('frontEnd/setCruiseTime', [this.deviceId, this.channelDeviceId, this.formId, dwellTime]))
chain.then(() => {
const idx = this.cruiseTours.findIndex(t => t.id === this.formId)
const presets = this.formPresets.map(p => ({
presetId: p.presetId,
dwellTime: p.dwellTime,
speed: p.speed
}))
const tour = { id: this.formId, name: this.formName, presets }
if (idx !== -1) {
this.$set(this.cruiseTours, idx, tour)
} else {
this.cruiseTours.push(tour)
}
this.cancelForm()
this.$message({ showClose: true, message: '保存成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error || '保存失败', type: 'error' })
}).finally(() => {
this.submitting = false
})
},
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'
clearCruiseTours() {
if (this.cruiseTours.length === 0) return
this.$confirm('确定清空所有巡航组?', '提示', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
}).then(() => {
this.deleting = true
this.$store.dispatch('frontEnd/deletePointForCruise', [this.deviceId, this.channelDeviceId, this.cruiseId, 0])
this.clearing = true
let chain = Promise.resolve()
this.cruiseTours.forEach(tour => {
chain = chain.then(() => this.$store.dispatch('frontEnd/deletePointForCruise', [this.deviceId, this.channelDeviceId, tour.id, 0]))
})
chain.then(() => {
this.cruiseTours = []
this.cruisingCruiseId = null
this.$message({ showClose: true, message: '清空成功', type: 'success' })
}).catch(() => {
this.$message({ showClose: true, message: '清空失败', type: 'error' })
}).finally(() => {
this.clearing = false
})
}).catch(() => {})
},
startCruise(row) {
this.operatingId = row.id
this.$store.dispatch('frontEnd/startCruise', [this.deviceId, this.channelDeviceId, row.id])
.then(() => {
this.cruisingCruiseId = row.id
this.$message({ showClose: true, message: '启用成功', type: 'success' })
}).catch(() => {
this.$message({ showClose: true, message: '启用失败', type: 'error' })
}).finally(() => {
this.operatingId = null
})
},
stopCruise(row) {
this.operatingId = row.id
this.$store.dispatch('frontEnd/stopCruise', [this.deviceId, this.channelDeviceId, row.id])
.then(() => {
this.cruisingCruiseId = null
this.$message({ showClose: true, message: '停止成功', type: 'success' })
}).catch(() => {
this.$message({ showClose: true, message: '停止失败', type: 'error' })
}).finally(() => {
this.operatingId = null
})
},
deleteCruise(row) {
this.$confirm('确定删除此巡航组?', '提示', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
}).then(() => {
this.deletingId = row.id
this.$store.dispatch('frontEnd/deletePointForCruise', [this.deviceId, this.channelDeviceId, row.id, 0])
.then(() => {
this.presetPoints = []
const idx = this.cruiseTours.indexOf(row)
if (idx !== -1) this.cruiseTours.splice(idx, 1)
if (this.cruisingCruiseId === row.id) this.cruisingCruiseId = null
this.$message({ showClose: true, message: '删除成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
}).catch(() => {
this.$message({ showClose: true, message: '删除失败', type: 'error' })
}).finally(() => {
this.deleting = false
this.deletingId = null
})
}).catch(() => {})
}