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>
|
||||
</el-table-column>
|
||||
<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 }">
|
||||
<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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -26,9 +31,10 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { getDevicesByPoint } from '../api/deviceManager'
|
||||
import VideoDeviceActions from './VideoDeviceActions.vue'
|
||||
|
||||
const props = defineProps({ selectedPoint: Object })
|
||||
defineEmits(['edit'])
|
||||
defineEmits(['edit', 'map', 'preview', 'ptz', 'playback', 'snapshot', 'syncChannels'])
|
||||
const devices = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -41,6 +47,7 @@ const loadDevices = async () => {
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
watch(() => props.selectedPoint, loadDevices, { immediate: true })
|
||||
defineExpose({ loadDevices })
|
||||
</script>
|
||||
|
||||
<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