Phase1_Day5_Video_components

This commit is contained in:
2026-05-16 23:55:39 +08:00
parent 7e77cc7db2
commit 6ebe327879
7 changed files with 294 additions and 3 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>