云台配置 优化巡航组配置

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-face {
font-family: "iconfont"; /* Project id 1291092 */ font-family: "iconfont"; /* Project id 1291092 */
src: url('iconfont.woff2?t=1769409737891') format('woff2') src: url('iconfont.woff2?t=1780559263294') format('woff2');
} }
.iconfont { .iconfont {
@ -11,6 +11,54 @@
-moz-osx-font-smoothing: grayscale; -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 { .icon-xintiao:before {
content: "\e7f4"; content: "\e7f4";
} }

Binary file not shown.

View File

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

View File

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

View File

@ -2,7 +2,7 @@
<div class="player-ptz-panel"> <div class="player-ptz-panel">
<div class="player-section"> <div class="player-section">
<div class="player-wrapper" :style="{ height: playerHeight }"> <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> </div>
<div class="ptz-section"> <div class="ptz-section">
@ -34,8 +34,6 @@ export default {
}, },
data() { data() {
return { return {
streamInfo: null,
videoUrl: '',
hasAudio: false, hasAudio: false,
playerHeight: '36vh' playerHeight: '36vh'
} }
@ -58,7 +56,7 @@ export default {
command: e.direction, command: e.direction,
horizonSpeed: speedVal, horizonSpeed: speedVal,
verticalSpeed: speedVal, verticalSpeed: speedVal,
zoomSpeed: speedVal zoomSpeed: parseInt(e.speed * 15 / 100)
}) })
}, },
onPtzStop() { onPtzStop() {
@ -88,12 +86,10 @@ export default {
startPlay() { startPlay() {
this.$store.dispatch('play/play', [this.deviceId, this.channelDeviceId]) this.$store.dispatch('play/play', [this.deviceId, this.channelDeviceId])
.then(data => { .then(data => {
this.streamInfo = data
this.hasAudio = data.hasAudio this.hasAudio = data.hasAudio
this.videoUrl = this.getUrlByStreamInfo(data)
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.playerTabs) { 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 }) this.$store.dispatch('play/stop', { deviceId: this.deviceId, channelId: this.channelDeviceId })
.catch(() => {}) .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> </script>
@ -125,7 +113,7 @@ export default {
height: 100%; height: 100%;
} }
.player-section { .player-section {
flex: 1; flex: 0.8;
} }
.ptz-section { .ptz-section {
flex-shrink: 0; flex-shrink: 0;

View File

@ -1,65 +1,76 @@
<template> <template>
<div style="height: 100%; display: flex; flex-direction: column;"> <div id="ptzCruiseConfig" 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;"> <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;">
<el-form-item label="巡航组号" style="margin-bottom: 0;"> <div>
<el-input-number v-model="cruiseId" :min="1" :max="255" controls-position="right" style="width: 140px" /> <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-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-item>
</el-form> </el-form>
<div v-if="presetPoints.length > 0" style="margin-bottom: 8px;"> <el-divider style="margin: 6px 0;" />
<el-tag <div style="margin-bottom: 4px;">
v-for="(item, index) in presetPoints" <el-button size="mini" type="primary" @click="addPresetRow">添加预置点</el-button>
:key="index"
closable
size="small"
style="margin-right: 8px; margin-bottom: 4px;"
@close="removePoint(item, index)"
>
{{ item.presetName || ('预置点' + item.presetId) }}
</el-tag>
</div> </div>
<div v-else style="color: #909399; font-size: 12px; margin-bottom: 8px;">暂无巡航点</div> <el-table :data="formPresets" size="mini" stripe border max-height="200px">
<el-form v-if="showAddPoint" size="mini" inline style="margin-bottom: 8px;"> <el-table-column label="序号" width="50">
<el-form-item style="margin-bottom: 0;"> <template v-slot="{ $index }">{{ $index + 1 }}</template>
<el-select v-model="selectedPreset" placeholder="选择预置点" style="width: 160px"> </el-table-column>
<el-option <el-table-column label="预置点" min-width="100">
v-for="p in allPresetList" <template v-slot="{ row }">
:key="p.presetId" <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)" :label="p.presetName || ('预置点' + p.presetId)"
:value="p" :value="p.presetId" />
/>
</el-select> </el-select>
</el-form-item> </template>
<el-form-item style="margin-bottom: 0;"> </el-table-column>
<el-button type="primary" @click="addPoint">确定</el-button> <el-table-column label="停留时间(秒)" min-width="100">
<el-button @click="showAddPoint = false">取消</el-button> <template v-slot="{ row }">
</el-form-item> <el-input-number v-model="row.dwellTime" :min="15" :max="300" size="mini" controls-position="right" style="width: 90px" />
</el-form> </template>
<el-button v-else size="small" style="margin-bottom: 8px;" @click="showAddPoint = true">添加巡航点</el-button> </el-table-column>
<el-form v-if="showSpeedInput" size="mini" inline style="margin-bottom: 8px;"> <el-table-column label="速度" min-width="100">
<el-form-item label="速度" style="margin-bottom: 0;"> <template v-slot="{ row }">
<el-input-number v-model="cruiseSpeed" :min="1" :max="255" controls-position="right" style="width: 120px" /> <el-select v-model="row.speed" size="mini" style="width: 90px">
</el-form-item> <el-option v-for="s in 10" :key="s" :label="s" :value="s" />
<el-form-item style="margin-bottom: 0;"> </el-select>
<el-button type="primary" @click="setSpeed">确定</el-button> </template>
<el-button @click="cancelSpeed">取消</el-button> </el-table-column>
</el-form-item> <el-table-column label="操作" width="60">
</el-form> <template v-slot="{ $index }">
<el-button v-else size="small" style="margin-bottom: 8px;" @click="openSpeed">设置巡航速度</el-button> <el-button size="mini" type="text" style="color: #F56C6C" @click="removePresetRow($index)">删除</el-button>
<el-form v-if="showTimeInput" size="mini" inline style="margin-bottom: 8px;"> </template>
<el-form-item label="停留时间(秒)" style="margin-bottom: 0;"> </el-table-column>
<el-input-number v-model="cruiseTime" :min="1" :max="300" controls-position="right" style="width: 120px" /> </el-table>
</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>
<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> </div>
</template> </template>
@ -72,18 +83,18 @@ export default {
}, },
data() { data() {
return { return {
cruiseId: 1, cruiseTours: [],
presetPoints: [], cruisingCruiseId: null,
allPresetList: [], formVisible: false,
selectedPreset: null, editingTourId: null,
showAddPoint: false, submitting: false,
showSpeedInput: false, clearing: false,
showTimeInput: false, operatingId: null,
cruiseSpeed: 5, deletingId: null,
cruiseTime: 15, formId: 1,
starting: false, formName: '',
stopping: false, formPresets: [],
deleting: false allPresetList: []
} }
}, },
created() { created() {
@ -99,99 +110,155 @@ export default {
console.log('[巡航] 加载预置点列表失败', error) console.log('[巡航] 加载预置点列表失败', error)
}) })
}, },
addPoint() { getNextAvailableId() {
if (!this.selectedPreset) { const used = new Set((this.cruiseTours || []).map(t => t.id))
this.$message({ showClose: true, message: '请选择预置点', type: 'warning' }) 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 return
} }
this.$store.dispatch('frontEnd/addPointForCruise', [this.deviceId, this.channelDeviceId, this.cruiseId, this.selectedPreset.presetId]) if (this.formId == null || this.formId < 0 || this.formId > 255) {
.then(() => { this.$message({ showClose: true, message: '巡航序号必须在0-255之间', type: 'warning' })
this.presetPoints.push(this.selectedPreset) return
this.selectedPreset = null }
this.showAddPoint = false this.submitting = true
this.$message({ showClose: true, message: '添加成功', type: 'success' }) let chain = Promise.resolve()
}).catch(error => { if (this.editingTourId !== null) {
this.$message({ showClose: true, message: error, type: 'error' }) 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
removePoint(preset, index) { const dwellTime = this.formPresets.length > 0 ? this.formPresets[0].dwellTime : 15
this.$store.dispatch('frontEnd/deletePointForCruise', [this.deviceId, this.channelDeviceId, this.cruiseId, preset.presetId]) chain = chain.then(() => this.$store.dispatch('frontEnd/setCruiseSpeed', [this.deviceId, this.channelDeviceId, this.formId, speed]))
.then(() => { chain = chain.then(() => this.$store.dispatch('frontEnd/setCruiseTime', [this.deviceId, this.channelDeviceId, this.formId, dwellTime]))
this.presetPoints.splice(index, 1) chain.then(() => {
this.$message({ showClose: true, message: '删除成功', type: 'success' }) 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 => { }).catch(error => {
this.$message({ showClose: true, message: error, type: '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(() => { }).finally(() => {
this.starting = false this.submitting = false
}) })
}, },
stopCruise() { clearCruiseTours() {
this.stopping = true if (this.cruiseTours.length === 0) return
this.$store.dispatch('frontEnd/stopCruise', [this.deviceId, this.channelDeviceId, this.cruiseId]) this.$confirm('确定清空所有巡航组?', '提示', {
.then(() => { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
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(() => { }).then(() => {
this.deleting = true this.clearing = true
this.$store.dispatch('frontEnd/deletePointForCruise', [this.deviceId, this.channelDeviceId, this.cruiseId, 0]) let chain = Promise.resolve()
.then(() => { this.cruiseTours.forEach(tour => {
this.presetPoints = [] chain = chain.then(() => this.$store.dispatch('frontEnd/deletePointForCruise', [this.deviceId, this.channelDeviceId, tour.id, 0]))
this.$message({ showClose: true, message: '删除成功', type: 'success' }) })
}).catch(error => { chain.then(() => {
this.$message({ showClose: true, message: error, type: 'error' }) this.cruiseTours = []
this.cruisingCruiseId = null
this.$message({ showClose: true, message: '清空成功', type: 'success' })
}).catch(() => {
this.$message({ showClose: true, message: '清空失败', type: 'error' })
}).finally(() => { }).finally(() => {
this.deleting = false 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(() => {
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(() => {
this.$message({ showClose: true, message: '删除失败', type: 'error' })
}).finally(() => {
this.deletingId = null
}) })
}).catch(() => {}) }).catch(() => {})
} }