diff --git a/Sense/ui/package.json b/Sense/ui/package.json index 11f00ba..c23c231 100644 --- a/Sense/ui/package.json +++ b/Sense/ui/package.json @@ -11,7 +11,8 @@ "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "eslint \"src/**/*.{js,vue}\"", - "test:navigation": "node --test tests/navigation.test.cjs" + "test:navigation": "node --test tests/navigation.test.cjs", + "test:device-payload": "node --test tests/devicePayload.test.mjs" }, "dependencies": { "@element-plus/icons-vue": "2.3.2", diff --git a/Sense/ui/src/views/sense/device/Devices.vue b/Sense/ui/src/views/sense/device/Devices.vue index 35d1526..e101b59 100644 --- a/Sense/ui/src/views/sense/device/Devices.vue +++ b/Sense/ui/src/views/sense/device/Devices.vue @@ -52,6 +52,7 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { Plus, Search } from '@element-plus/icons-vue' import { useStore } from 'vuex' import { createDevice, disableDevice, listDevices, updateCredential, updateDevice } from '../../../api/sense/device' +import { createDevicePayload, updateDevicePayload } from './devicePayload.mjs' const store = useStore() const loading = ref(false), saving = ref(false), deviceDialog = ref(false), credentialDialog = ref(false) @@ -69,7 +70,7 @@ function reset() { Object.assign(query, { keyword: '', page: 1, page_size: 20 }) 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, { 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 saveDevice() { const valid = await deviceFormRef.value?.validate().catch(() => false); if (!valid) return; saving.value = true; try { if (editing.value) await updateDevice(editing.value.id, updateDevicePayload(deviceForm, editing.value.version)); else await createDevice(createDevicePayload(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, { 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) diff --git a/Sense/ui/src/views/sense/device/devicePayload.mjs b/Sense/ui/src/views/sense/device/devicePayload.mjs new file mode 100644 index 0000000..2579d8f --- /dev/null +++ b/Sense/ui/src/views/sense/device/devicePayload.mjs @@ -0,0 +1,17 @@ +export function createDevicePayload(form) { + return { + name: form.name, + location: form.location, + modality: form.modality, + capabilities: [...form.capabilities] + } +} + +export function updateDevicePayload(form, version) { + return { + name: form.name, + location: form.location, + capabilities: [...form.capabilities], + version + } +} diff --git a/Sense/ui/tests/devicePayload.test.mjs b/Sense/ui/tests/devicePayload.test.mjs new file mode 100644 index 0000000..19f3af0 --- /dev/null +++ b/Sense/ui/tests/devicePayload.test.mjs @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { createDevicePayload, updateDevicePayload } from '../src/views/sense/device/devicePayload.mjs' + +const form = { + name: '东门摄像机', + location: '教学楼一楼东门', + modality: 'video', + capabilities: ['video'], + id: 'read-only-id' +} + +test('create payload keeps Chinese values and includes modality', () => { + assert.deepEqual(createDevicePayload(form), { + name: '东门摄像机', + location: '教学楼一楼东门', + modality: 'video', + capabilities: ['video'] + }) +}) + +test('update payload uses the PATCH allowlist and excludes modality', () => { + const payload = updateDevicePayload(form, 9) + assert.deepEqual(payload, { + name: '东门摄像机', + location: '教学楼一楼东门', + capabilities: ['video'], + version: 9 + }) + assert.equal('modality' in payload, false) + assert.equal('id' in payload, false) +})