feat: add controlled ONVIF and RTSP admission (#24)

This commit is contained in:
QiuSW
2026-08-12 17:14:34 +08:00
parent 5b8f4a4c8c
commit 4f17cca91f
18 changed files with 713 additions and 0 deletions
@@ -0,0 +1,6 @@
import request from '../../../bootstrap/request'
export const discoverDevices = () => request.get('/admission/discover')
export const probeDevice = (data) => request.post('/admission/probe', data)
export const admissionResult = (deviceID) => request.get(`/admission/devices/${deviceID}`)
@@ -0,0 +1,3 @@
import Admission from '../../views/sense/admission/Admission.vue'
export default { path: 'admission', name: 'sense-admission', component: Admission, meta: { title: '视频接入', icon: 'Connection', order: 20, permission: 'device.write' } }
@@ -0,0 +1,52 @@
<template>
<section class="page-container">
<div class="page-heading"><div><h1>视频接入</h1><p>选择已登记设备,填写 ONVIF 服务地址并检查主、子码流。</p></div></div>
<el-row :gutter="16">
<el-col :span="9">
<el-card shadow="never">
<template #header><strong>接入检查</strong></template>
<el-alert title="发现功能只在实施人员配置获准网卡后启用,不会扫描其他网络。" type="info" show-icon :closable="false" />
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="admission-form">
<el-form-item label="设备" prop="device_id"><el-select v-model="form.device_id" filterable placeholder="选择已登记的视频设备" style="width: 100%"><el-option v-for="item in devices" :key="item.id" :label="`${item.name} · ${item.location || '未填写位置'}`" :value="item.id" /></el-select></el-form-item>
<el-form-item label="ONVIF 服务地址" prop="address"><el-input v-model="form.address" placeholder="例如:http://设备地址/onvif/device_service" /><div class="field-hint">地址中不能包含用户名或密码;凭据来自设备管理中的安全配置。</div></el-form-item>
<el-form-item><el-button type="primary" :loading="probing" @click="probe">检查设备与视频</el-button><el-button :loading="discovering" @click="discover">发现设备</el-button></el-form-item>
</el-form>
</el-card>
</el-col>
<el-col :span="15">
<el-card shadow="never">
<template #header><div class="result-header"><strong>检查结果</strong><el-tag v-if="result.status" :type="result.status === 'ready' ? 'success' : 'warning'">{{ statusLabel(result.status) }}</el-tag></div></template>
<el-empty v-if="!result.status" description="选择设备并开始检查" />
<template v-else>
<el-result :icon="result.status === 'ready' ? 'success' : 'warning'" :title="statusLabel(result.status)" :sub-title="result.detail" />
<el-table v-if="result.profiles?.length" :data="result.profiles" border>
<el-table-column label="用途" width="90"><template #default="scope"><el-tag>{{ scope.row.kind === 'main' ? '主码流' : scope.row.kind === 'sub' ? '子码流' : '其他' }}</el-tag></template></el-table-column>
<el-table-column prop="name" label="Profile" min-width="120" />
<el-table-column label="分辨率" width="120"><template #default="scope">{{ scope.row.width }} × {{ scope.row.height }}</template></el-table-column>
<el-table-column prop="encoding" label="编码" width="90" />
<el-table-column label="视频验证" min-width="150"><template #default="scope"><el-tag :type="scope.row.verification.status === 'ready' ? 'success' : 'danger'">{{ scope.row.verification.detail || scope.row.verification.status }}</el-tag></template></el-table-column>
</el-table>
</template>
</el-card>
</el-col>
</el-row>
<el-dialog v-model="discoveryDialog" title="发现结果" width="680px"><el-empty v-if="!discovered.length" description="当前获准网卡未发现设备" /><el-table v-else :data="discovered.map(address => ({ address }))" border><el-table-column prop="address" label="ONVIF 地址" /><el-table-column label="操作" width="90"><template #default="scope"><el-button link type="primary" @click="useAddress(scope.row.address)">使用</el-button></template></el-table-column></el-table></el-dialog>
</section>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { listDevices } from '../../../api/sense/device'
import { discoverDevices, probeDevice } from '../../../api/sense/admission'
const devices=ref([]),probing=ref(false),discovering=ref(false),discoveryDialog=ref(false),discovered=ref([]),formRef=ref()
const form=reactive({device_id:'',address:''}),result=reactive({})
const rules={device_id:[{required:true,message:'请选择设备',trigger:'change'}],address:[{required:true,message:'请输入 ONVIF 服务地址',trigger:'blur'},{validator:(_r,v,done)=>v.includes('@')?done(new Error('地址中不能包含凭据')):done(),trigger:'blur'}]}
function statusLabel(value){return({ready:'接入正常',profile_failed:'部分码流失败',authentication_failed:'认证失败',timeout:'响应超时',clock_skew:'需要校时',unreachable:'设备不可达'})[value]||value}
async function loadDevices(){devices.value=(await listDevices({page:1,page_size:100})).items.filter(item=>item.modality==='video'&&item.status!=='disabled')}
async function probe(){const valid=await formRef.value?.validate().catch(()=>false);if(!valid)return;probing.value=true;try{Object.assign(result,await probeDevice(form))}catch(error){ElMessage.error(error.message||'检查失败')}finally{probing.value=false}}
async function discover(){discovering.value=true;try{discovered.value=(await discoverDevices()).items||[];discoveryDialog.value=true}catch(error){ElMessage.warning(error.message||'未配置获准发现网卡')}finally{discovering.value=false}}
function useAddress(value){form.address=value;discoveryDialog.value=false}
onMounted(loadDevices)
</script>
<style scoped>.admission-form{margin-top:18px}.field-hint{color:#86909c;font-size:12px;line-height:1.5}.result-header{display:flex;align-items:center;justify-content:space-between}</style>