feat: split camera credentials and persist profiles (#50)
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
<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="设备" prop="device_id"><el-select v-model="form.device_id" filterable placeholder="选择已登记的视频设备" style="width: 100%" @change="loadSavedResult"><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>
|
||||
@@ -38,12 +38,13 @@
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { listDevices } from '../../../api/sense/device'
|
||||
import { discoverDevices, probeDevice } from '../../../api/sense/admission'
|
||||
import { admissionResult, 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 loadSavedResult(){Object.keys(result).forEach(key=>delete result[key]);if(!form.device_id)return;try{Object.assign(result,await admissionResult(form.device_id))}catch{/* 尚未接入时保持空状态 */}}
|
||||
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}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<el-table-column prop="location" label="安装位置" min-width="150" />
|
||||
<el-table-column label="类型" width="110"><template #default="scope">{{ scope.row.modality === 'video' ? '视频设备' : scope.row.modality }}</template></el-table-column>
|
||||
<el-table-column label="接入能力" width="140"><template #default="scope"><el-tag :type="scope.row.adapter_status === 'ready' ? 'success' : 'warning'">{{ scope.row.adapter_status === 'ready' ? '可接入' : '适配器未就绪' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="凭据" width="110"><template #default="scope"><el-tag :type="scope.row.credential_configured ? 'success' : 'info'">{{ scope.row.credential_configured ? '已配置' : '未配置' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="设备凭据" width="150"><template #default="scope"><el-tag :type="scope.row.credential_configured && scope.row.rtsp_credential_configured ? 'success' : 'info'">{{ scope.row.credential_configured && scope.row.rtsp_credential_configured ? '已配置' : '未完整配置' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="状态" width="100"><template #default="scope"><el-tag :type="scope.row.status === 'disabled' ? 'info' : 'primary'">{{ statusLabel(scope.row.status) }}</el-tag></template></el-table-column>
|
||||
<el-table-column v-if="canWrite" label="操作" width="210" fixed="right"><template #default="scope"><el-button link type="primary" @click="openEdit(scope.row)">编辑</el-button><el-button link type="primary" @click="openCredential(scope.row)">更新凭据</el-button><el-button v-if="scope.row.status !== 'disabled'" link type="danger" @click="disable(scope.row)">停用</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
@@ -33,8 +33,13 @@
|
||||
<el-dialog v-model="credentialDialog" title="更新设备凭据" width="520px" destroy-on-close>
|
||||
<el-alert title="凭据保存后不能查看,只能再次更新。请勿把密码写入设备地址。" type="warning" show-icon :closable="false" />
|
||||
<el-form ref="credentialFormRef" :model="credentialForm" :rules="credentialRules" label-width="82px" class="credential-form">
|
||||
<el-form-item label="用户名" prop="username"><el-input v-model="credentialForm.username" autocomplete="off" /></el-form-item>
|
||||
<el-form-item label="密码" prop="password"><el-input v-model="credentialForm.password" type="password" show-password autocomplete="new-password" /></el-form-item>
|
||||
<el-form-item label="ONVIF 用户名" prop="onvif_username"><el-input v-model="credentialForm.onvif_username" autocomplete="off" /></el-form-item>
|
||||
<el-form-item label="ONVIF 密码" prop="onvif_password"><el-input v-model="credentialForm.onvif_password" type="password" show-password autocomplete="new-password" /></el-form-item>
|
||||
<el-form-item label-width="0"><el-checkbox v-model="credentialForm.rtsp_same_as_onvif">RTSP 与 ONVIF 使用相同账号</el-checkbox></el-form-item>
|
||||
<template v-if="!credentialForm.rtsp_same_as_onvif">
|
||||
<el-form-item label="RTSP 用户名" prop="rtsp_username"><el-input v-model="credentialForm.rtsp_username" autocomplete="off" /></el-form-item>
|
||||
<el-form-item label="RTSP 密码" prop="rtsp_password"><el-input v-model="credentialForm.rtsp_password" type="password" show-password autocomplete="new-password" /></el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="credentialDialog = false">取消</el-button><el-button type="primary" :loading="saving" @click="saveCredential">安全保存</el-button></template>
|
||||
</el-dialog>
|
||||
@@ -54,18 +59,18 @@ const editing = ref(null), credentialTarget = ref(null), deviceFormRef = ref(),
|
||||
const query = reactive({ keyword: '', page: 1, page_size: 20 })
|
||||
const page = reactive({ items: [], total: 0 })
|
||||
const deviceForm = reactive({ name: '', location: '', modality: 'video', capabilities: ['video'] })
|
||||
const credentialForm = reactive({ username: '', password: '' })
|
||||
const credentialForm = reactive({ onvif_username: '', onvif_password: '', rtsp_same_as_onvif: true, rtsp_username: '', rtsp_password: '' })
|
||||
const deviceRules = { name: [{ required: true, message: '请输入设备名称', trigger: 'blur' }], modality: [{ required: true, message: '请选择类型', trigger: 'change' }] }
|
||||
const credentialRules = { username: [{ required: true, message: '请输入用户名', trigger: 'blur' }], password: [{ required: true, message: '请输入密码', trigger: 'blur' }] }
|
||||
const credentialRules = { onvif_username: [{ required: true, message: '请输入 ONVIF 用户名', trigger: 'blur' }], onvif_password: [{ required: true, message: '请输入 ONVIF 密码', trigger: 'blur' }], rtsp_username: [{ validator: (_r, value, done) => !credentialForm.rtsp_same_as_onvif && !value ? done(new Error('请输入 RTSP 用户名')) : done(), trigger: 'blur' }], rtsp_password: [{ validator: (_r, value, done) => !credentialForm.rtsp_same_as_onvif && !value ? done(new Error('请输入 RTSP 密码')) : done(), trigger: 'blur' }] }
|
||||
const canWrite = computed(() => store.getters['sense-identity/hasPermission']?.('device.write'))
|
||||
function statusLabel(value) { return ({ pending: '待接入', active: '正常', offline: '离线', disabled: '已停用' })[value] || value }
|
||||
async function load() { loading.value = true; try { Object.assign(page, await listDevices(query)) } finally { loading.value = false } }
|
||||
function reset() { Object.assign(query, { keyword: '', page: 1, page_size: 20 }); load() }
|
||||
function openCreate() { editing.value = null; Object.assign(deviceForm, { name: '', location: '', modality: 'video', capabilities: ['video'] }); deviceDialog.value = true }
|
||||
function openEdit(row) { editing.value = row; Object.assign(deviceForm, { name: row.name, location: row.location, modality: row.modality, capabilities: row.capabilities }); deviceDialog.value = true }
|
||||
function openCredential(row) { credentialTarget.value = row; Object.assign(credentialForm, { username: '', password: '' }); credentialDialog.value = true }
|
||||
function openCredential(row) { credentialTarget.value = row; Object.assign(credentialForm, { onvif_username: '', onvif_password: '', rtsp_same_as_onvif: row.rtsp_credential_same_as_onvif !== false, rtsp_username: '', rtsp_password: '' }); credentialDialog.value = true }
|
||||
async function saveDevice() { const valid = await deviceFormRef.value?.validate().catch(() => false); if (!valid) return; saving.value = true; try { if (editing.value) await updateDevice(editing.value.id, { ...deviceForm, version: editing.value.version }); else await createDevice(deviceForm); ElMessage.success('设备已保存'); deviceDialog.value = false; await load() } catch (error) { ElMessage.error(error.message || '保存失败') } finally { saving.value = false } }
|
||||
async function saveCredential() { const valid = await credentialFormRef.value?.validate().catch(() => false); if (!valid) return; saving.value = true; try { await updateCredential(credentialTarget.value.id, credentialForm); Object.assign(credentialForm, { username: '', password: '' }); ElMessage.success('凭据已安全更新'); credentialDialog.value = false; await load() } catch (error) { ElMessage.error(error.message || '更新失败') } finally { saving.value = false } }
|
||||
async function saveCredential() { const valid = await credentialFormRef.value?.validate().catch(() => false); if (!valid) return; saving.value = true; try { await updateCredential(credentialTarget.value.id, credentialForm); Object.assign(credentialForm, { onvif_username: '', onvif_password: '', rtsp_same_as_onvif: true, rtsp_username: '', rtsp_password: '' }); ElMessage.success('ONVIF 与 RTSP 凭据已安全更新'); credentialDialog.value = false; await load() } catch (error) { ElMessage.error(error.message || '更新失败') } finally { saving.value = false } }
|
||||
async function disable(row) { await ElMessageBox.confirm(`停用“${row.name}”后将停止后续接入,设备记录和审计仍保留。`, '确认停用', { type: 'warning' }); await disableDevice(row.id, row.version); ElMessage.success('设备已停用'); await load() }
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user