Phase1_Day5_Video_components
This commit is contained in:
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" title="编辑设备" width="500px">
|
||||||
|
<el-form :model="form" label-width="100px">
|
||||||
|
<el-form-item label="名称">
|
||||||
|
<el-input v-model="form.deviceName" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="种类">
|
||||||
|
<el-select v-model="form.deviceCategory" style="width:100%">
|
||||||
|
<el-option label="摄像机" value="摄像机"/><el-option label="硬盘录像机" value="硬盘录像机"/>
|
||||||
|
<el-option label="空调控制器" value="空调控制器"/><el-option label="温湿度变送器" value="温湿度变送器"/>
|
||||||
|
<el-option label="除湿/恒湿机" value="除湿/恒湿机"/><el-option label="烟雾报警器" value="烟雾报警器"/>
|
||||||
|
<el-option label="气体报警器" value="气体报警器"/><el-option label="门磁" value="门磁"/>
|
||||||
|
<el-option label="空调" value="空调"/><el-option label="智能断路器" value="智能断路器"/>
|
||||||
|
<el-option label="人行道闸" value="人行道闸"/><el-option label="车辆道闸" value="车辆道闸"/>
|
||||||
|
<el-option label="485钥匙柜" value="485钥匙柜"/><el-option label="网络钥匙柜" value="网络钥匙柜"/>
|
||||||
|
<el-option label="门禁一体机" value="门禁一体机"/><el-option label="红外报警器" value="红外报警器"/>
|
||||||
|
<el-option label="紧急报警按钮" value="紧急报警按钮"/><el-option label="动环采集器" value="动环采集器"/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分组">
|
||||||
|
<el-select v-model="form.deviceGroup" style="width:100%">
|
||||||
|
<el-option v-for="g in groups" :key="g" :label="g" :value="g" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="安装位置">
|
||||||
|
<el-input v-model="form.location" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="启用">
|
||||||
|
<el-switch v-model="form.enable" active-value="启用" inactive-value="禁用" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.remark" type="textarea" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="save">保存</el-button>
|
||||||
|
<el-button @click="visible=false">取消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, watch } from 'vue'
|
||||||
|
import { updateDevice } from '../api/deviceManager'
|
||||||
|
|
||||||
|
const props = defineProps({ modelValue: Boolean, device: Object })
|
||||||
|
const emit = defineEmits(['update:modelValue', 'saved'])
|
||||||
|
const visible = computed({ get: ()=>props.modelValue, set: (v)=>emit('update:modelValue', v) })
|
||||||
|
const groups = ['视频设备','IoT设备','门禁设备','道闸设备','报警设备']
|
||||||
|
const form = reactive({ deviceName:'', deviceCategory:'', deviceGroup:'', location:'', enable:'启用', remark:'' })
|
||||||
|
|
||||||
|
watch(() => props.device, (d) => {
|
||||||
|
if (d) Object.assign(form, d)
|
||||||
|
})
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
await updateDevice(props.device.deviceId, form)
|
||||||
|
emit('saved')
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" title="实时预览" width="960px" @close="stopPlay">
|
||||||
|
<div class="player-container">
|
||||||
|
<div v-if="!streamUrl" class="loading">加载中...</div>
|
||||||
|
<video v-else ref="videoRef" :src="flvUrl" autoplay muted controls style="width:100%;height:500px"></video>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible=false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({ modelValue: Boolean, device: Object })
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (v) => emit('update:modelValue', v)
|
||||||
|
})
|
||||||
|
const videoRef = ref(null)
|
||||||
|
const streamUrl = ref(null)
|
||||||
|
|
||||||
|
const flvUrl = computed(() => streamUrl.value?.wsFlv || streamUrl.value?.httpFlv)
|
||||||
|
|
||||||
|
const openPreview = async (gatewayBaseUrl, adapter, deviceId) => {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${gatewayBaseUrl}/api/gateway/streams/${adapter}/${deviceId}/live`)
|
||||||
|
streamUrl.value = await resp.json()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Get stream URL failed', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopPlay = () => {
|
||||||
|
streamUrl.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.device, (dev) => {
|
||||||
|
if (dev && visible.value) {
|
||||||
|
openPreview('http://localhost:5100', dev.adapterCode, dev.sourceId)
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.player-container { min-height: 300px; display: flex; align-items: center; justify-content: center; background: #000; }
|
||||||
|
.loading { color: #fff; }
|
||||||
|
</style>
|
||||||
@@ -14,9 +14,14 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="adapterCode" label="来源" width="120" />
|
<el-table-column prop="adapterCode" label="来源" width="120" />
|
||||||
<el-table-column label="操作" width="120" fixed="right">
|
<el-table-column label="操作" width="280" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button size="small" text @click="$emit('edit', row)">编辑</el-button>
|
<VideoDeviceActions v-if="row.deviceGroup==='视频设备'"
|
||||||
|
@preview="$emit('preview',row)" @ptz="$emit('ptz',row)"
|
||||||
|
@playback="$emit('playback',row)" @snapshot="$emit('snapshot',row)"
|
||||||
|
@syncChannels="$emit('syncChannels',row)" />
|
||||||
|
<el-button v-else size="small" text @click="$emit('edit', row)">编辑</el-button>
|
||||||
|
<el-button size="small" text @click="$emit('map', row)">地图</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -26,9 +31,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { getDevicesByPoint } from '../api/deviceManager'
|
import { getDevicesByPoint } from '../api/deviceManager'
|
||||||
|
import VideoDeviceActions from './VideoDeviceActions.vue'
|
||||||
|
|
||||||
const props = defineProps({ selectedPoint: Object })
|
const props = defineProps({ selectedPoint: Object })
|
||||||
defineEmits(['edit'])
|
defineEmits(['edit', 'map', 'preview', 'ptz', 'playback', 'snapshot', 'syncChannels'])
|
||||||
const devices = ref([])
|
const devices = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
@@ -41,6 +47,7 @@ const loadDevices = async () => {
|
|||||||
} finally { loading.value = false }
|
} finally { loading.value = false }
|
||||||
}
|
}
|
||||||
watch(() => props.selectedPoint, loadDevices, { immediate: true })
|
watch(() => props.selectedPoint, loadDevices, { immediate: true })
|
||||||
|
defineExpose({ loadDevices })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" title="地图绑定" width="600px">
|
||||||
|
<el-form :model="form" label-width="100px">
|
||||||
|
<el-form-item label="模型ID">
|
||||||
|
<el-input v-model="form.mapModelId" placeholder="VgoMap模型ID" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="缩放">
|
||||||
|
<el-input-number v-model="form.mapModelScale" :min="0.1" :max="10" :step="0.1" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="旋转角度">
|
||||||
|
<el-input v-model="form.mapModelRotation" placeholder='JSON, 如 {"x":0,"y":90,"z":0}' />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="save">保存</el-button>
|
||||||
|
<el-button @click="visible=false">取消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, watch } from 'vue'
|
||||||
|
import { updateDevice } from '../api/deviceManager'
|
||||||
|
|
||||||
|
const props = defineProps({ modelValue: Boolean, device: Object })
|
||||||
|
const emit = defineEmits(['update:modelValue', 'saved'])
|
||||||
|
const visible = computed({ get: ()=>props.modelValue, set: (v)=>emit('update:modelValue', v) })
|
||||||
|
const form = reactive({ mapModelId: '', mapModelScale: 1, mapModelRotation: '' })
|
||||||
|
|
||||||
|
watch(() => props.device, (d) => {
|
||||||
|
if (d) {
|
||||||
|
form.mapModelId = d.mapModelId || ''
|
||||||
|
form.mapModelScale = d.mapModelScale || 1
|
||||||
|
form.mapModelRotation = d.mapModelRotation || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
await updateDevice(props.device.deviceId, form)
|
||||||
|
emit('saved')
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" title="云台控制" width="320px">
|
||||||
|
<div class="ptz-pad">
|
||||||
|
<el-button circle @mousedown="startPtz('up')" @mouseup="stopPtz" @mouseleave="stopPtz" :disabled="moving">
|
||||||
|
<el-icon><ArrowUp /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<div class="ptz-row">
|
||||||
|
<el-button circle @mousedown="startPtz('left')" @mouseup="stopPtz" :disabled="moving"><el-icon><ArrowLeft /></el-icon></el-button>
|
||||||
|
<el-button circle type="danger" @click="sendPtz('stop')"><el-icon><Close /></el-icon></el-button>
|
||||||
|
<el-button circle @mousedown="startPtz('right')" @mouseup="stopPtz" :disabled="moving"><el-icon><ArrowRight /></el-icon></el-button>
|
||||||
|
</div>
|
||||||
|
<el-button circle @mousedown="startPtz('down')" @mouseup="stopPtz" :disabled="moving"><el-icon><ArrowDown /></el-icon></el-button>
|
||||||
|
<div class="ptz-row" style="margin-top:8px">
|
||||||
|
<el-button circle @mousedown="startPtz('zoom_in')" @mouseup="stopPtz">+</el-button>
|
||||||
|
<el-button circle @mousedown="startPtz('zoom_out')" @mouseup="stopPtz">-</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({ modelValue: Boolean, device: Object })
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const visible = computed({ get: ()=>props.modelValue, set: (v)=>emit('update:modelValue', v) })
|
||||||
|
const moving = ref(false)
|
||||||
|
|
||||||
|
const sendPtz = async (dir) => {
|
||||||
|
try {
|
||||||
|
await fetch(`http://localhost:5100/api/gateway/streams/${props.device?.adapterCode}/${props.device?.sourceId}/ptz`, {
|
||||||
|
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||||
|
body: JSON.stringify({ direction: dir, speed: 0.5 })
|
||||||
|
})
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPtz = async (dir) => {
|
||||||
|
moving.value = true
|
||||||
|
await sendPtz(dir)
|
||||||
|
}
|
||||||
|
const stopPtz = async () => {
|
||||||
|
moving.value = false
|
||||||
|
await sendPtz('stop')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ptz-pad { display: flex; flex-direction: column; align-items: center; gap: 4px; }
|
||||||
|
.ptz-row { display: flex; gap: 8px; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<div class="video-actions">
|
||||||
|
<el-button size="small" type="primary" @click="$emit('preview')">
|
||||||
|
<el-icon><VideoPlay /></el-icon> 实时预览
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" @click="$emit('ptz')">
|
||||||
|
<el-icon><Aim /></el-icon> 云台控制
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" @click="$emit('playback')">
|
||||||
|
<el-icon><VideoCamera /></el-icon> 查看回放
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" @click="$emit('snapshot')">
|
||||||
|
<el-icon><Camera /></el-icon> 获取快照
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" type="warning" @click="$emit('syncChannels')">
|
||||||
|
同步通道
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineEmits(['preview', 'ptz', 'playback', 'snapshot', 'syncChannels'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.video-actions { display: flex; gap: 4px; flex-wrap: wrap; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div class="device-manager-page">
|
||||||
|
<div class="left-panel">
|
||||||
|
<RegionTree @select-point="onSelectPoint" />
|
||||||
|
</div>
|
||||||
|
<div class="right-panel">
|
||||||
|
<DeviceTable ref="tableRef" :selectedPoint="selectedPoint"
|
||||||
|
@edit="onEdit" @map="onMap" @preview="onPreview" @ptz="onPtz"
|
||||||
|
@playback="onPlayback" @snapshot="onSnapshot" @syncChannels="onSyncChannels" />
|
||||||
|
</div>
|
||||||
|
<!-- Dialogs -->
|
||||||
|
<DeviceLivePreview v-model="previewVisible" :device="currentDevice" />
|
||||||
|
<PtzControlPanel v-model="ptzVisible" :device="currentDevice" />
|
||||||
|
<MapBindingPanel v-model="mapVisible" :device="currentDevice" @saved="refresh" />
|
||||||
|
<DeviceEditDialog v-model="editVisible" :device="currentDevice" @saved="refresh" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import RegionTree from './components/RegionTree.vue'
|
||||||
|
import DeviceTable from './components/DeviceTable.vue'
|
||||||
|
import DeviceLivePreview from './components/DeviceLivePreview.vue'
|
||||||
|
import PtzControlPanel from './components/PtzControlPanel.vue'
|
||||||
|
import MapBindingPanel from './components/MapBindingPanel.vue'
|
||||||
|
import DeviceEditDialog from './components/DeviceEditDialog.vue'
|
||||||
|
|
||||||
|
const selectedPoint = ref(null)
|
||||||
|
const tableRef = ref(null)
|
||||||
|
const currentDevice = ref(null)
|
||||||
|
const previewVisible = ref(false)
|
||||||
|
const ptzVisible = ref(false)
|
||||||
|
const mapVisible = ref(false)
|
||||||
|
const editVisible = ref(false)
|
||||||
|
|
||||||
|
const onSelectPoint = (point) => { selectedPoint.value = point }
|
||||||
|
|
||||||
|
const onPreview = (d) => { currentDevice.value = d; previewVisible.value = true }
|
||||||
|
const onPtz = (d) => { currentDevice.value = d; ptzVisible.value = true }
|
||||||
|
const onMap = (d) => { currentDevice.value = d; mapVisible.value = true }
|
||||||
|
const onEdit = (d) => { currentDevice.value = d; editVisible.value = true }
|
||||||
|
const onPlayback = (d) => { console.log('playback', d) }
|
||||||
|
const onSnapshot = (d) => { console.log('snapshot', d) }
|
||||||
|
const onSyncChannels = (d) => { console.log('syncChannels', d) }
|
||||||
|
const refresh = () => { tableRef.value?.loadDevices() }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.device-manager-page { display: flex; height: calc(100vh - 80px); }
|
||||||
|
.left-panel { width: 280px; border-right: 1px solid #e4e7ed; overflow-y: auto; }
|
||||||
|
.right-panel { flex: 1; overflow-y: auto; }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user