'use strict'; const $ = (s) => document.querySelector(s); const $$ = (s) => [...document.querySelectorAll(s)]; const esc = (s) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const kind = (v) => ({home:'家庭',company:'办公',office:'办公',factory:'工厂',other:'其他'}[v] || '其他'); let state = {cameras:[], assets:[], recorders:[], devices:[], sites:[], storage:{}, scan:{}}, activePage = 'households', loggedIn = false, setup = false; let players = [], liveKey = '', spans = [], queryStart = null, queryEnd = null, queryCamera = '', toastTimer, playbackGeneration = 0; let currentUser = null, sessionGeneration = 0, refreshGeneration = 0, authGeneration = 0; let householdScope = ''; const isPlatformAdmin = () => currentUser?.role === 'admin'; const isAdmin = () => ['admin','family_admin'].includes(currentUser?.role); const roleName = user => {const h=(state.households||[]).find(h=>h.id===state.familyId)||{},home=!h.kind||h.kind==='home';return user.role==='admin'?'超级管理员':user.role==='family_admin'?(h.ownerId===user.id?(home?'一家之主':'场所负责人'):(home?'家庭管理员':'场所管理员')):(home?'家庭成员':'场所成员');}; function applyPermissions(){ const admin=isAdmin(); $('nav [data-page="storage"]').hidden=!isPlatformAdmin(); $('#account-password').hidden=!loggedIn; $$('[data-admin],nav [data-page="recorders"],nav [data-page="cameras"],nav [data-page="sites"],nav [data-page="storage"],[data-action],[data-edit-camera],[data-camera-settings]').forEach(el=>el.hidden=!admin); $('nav [data-page="storage"]').hidden=!isPlatformAdmin(); $('.discovery').hidden=!isPlatformAdmin()||!!deviceContext; $('#scan-result').hidden=!isPlatformAdmin(); $('#new-household').hidden=!isPlatformAdmin(); $('#household-context').hidden=!loggedIn; $('#household-filter').disabled=!isPlatformAdmin(); if(typeof renderSteward==='function')renderSteward(); $('#current-user').textContent=currentUser ? currentUser.name+' · '+roleName(currentUser) : ''; } function clearPrivateView(){ if(typeof resetAppearance==='function')resetAppearance(); if(typeof clearSnapshot==='function')clearSnapshot(); if(typeof resetDeviceView==='function')resetDeviceView(); if(typeof resetSteward==='function')resetSteward(); if(typeof resetHeritage==='function')resetHeritage(); if(typeof clearCameraSettings==='function')clearCameraSettings(); sessionGeneration++;refreshGeneration++;authGeneration++;loggedIn=false; if(typeof closePtz==='function')closePtz();if(document.fullscreenElement)document.exitFullscreen().catch(()=>{}); currentUser=null;householdScope='';stopPlayers();resetPlayback(); state={cameras:[],assets:[],recorders:[],devices:[],sites:[],storage:{},scan:{}}; for(const id of ['live-grid','asset-list','camera-list','recorder-list','site-list','audit-list','family-people','family-detail','family-tree','user-list','household-list'])$('#'+id).replaceChildren(); if(typeof clearFamilyState==='function')clearFamilyState(); $$('dialog[open]').filter(d=>d.id!=='auth-dialog').forEach(d=>d.close()); $$('dialog form').forEach(f=>f.reset()); activePage='households';showPage('households');applyPermissions(); } const pageInfo = {live:['实时画面','从一个空间,看到每一个位置。'],playback:['录像回放','沿着时间,找回需要的画面。'],cameras:['镜头通道','为摄像头的每个镜头配置独立通道。'],objects:['设备管理','按类型、区域、负责人或所有人,找到每一台设备。'],recorders:['录像机','连接现有录像与各个现场通道。'],sites:['区域与空间','层级与名称由你定义,设备各有归属。'],storage:['录像与存储','让记录持续,也让磁盘留有余地。']}; async function api(path, data) { const generation=sessionGeneration; const r = await fetch(path, {method:data === undefined ? 'GET' : 'POST', headers:{...(data === undefined ? {} : {'Content-Type':'application/json'}),...(typeof householdScope!=='undefined'&&householdScope?{'X-Household-Id':householdScope}:{})}, body:data === undefined ? undefined : JSON.stringify(data)}); let result; try { result = await r.json(); } catch { throw Error('服务返回异常,请稍后重试'); } if(generation!==sessionGeneration)throw Object.assign(Error('登录会话已变化,请重新操作'),{stale:true}); if (!r.ok) { if (r.status === 401 && loggedIn) { clearPrivateView(); auth(); } throw Object.assign(Error(result.error || '操作未完成'),{status:r.status}); } return result; } function toast(text) { clearTimeout(toastTimer); $('#toast').textContent = text; $('#toast').hidden = false; toastTimer = setTimeout(() => $('#toast').hidden = true, 4000); } function siteName(id) { return ZhaoDevices.path(state.sites,id).map(s=>s.name).join(' / ') || '未关联空间'; } function address(s) { return [s.province,s.city,s.district,s.street,s.community,s.building,s.unit,s.floor,s.door].filter(Boolean).join(' · ') || '地址待完善'; } function filtered() { const range=$('#site-filter').value?ZhaoDevices.descendants(state.sites,$('#site-filter').value):null;const cameras=state.cameras.filter(c=>!range||range.has(c.siteId));return typeof deviceCameras==='function'?deviceCameras(cameras):cameras; } function setOptions(select, html) { const old = select.value; select.innerHTML = html; if ([...select.options].some(o => o.value === old)) select.value = old; } function stopPlayers() { players.forEach(p => p.destroy()); players = []; $$('#live-grid video').forEach(v => {v.pause();v.removeAttribute('src');v.load();}); liveKey = ''; } function showPage(page, options={}) { if(typeof leaveSettings==='function'&&activePage==='settings'&&page!=='settings')leaveSettings(); if(!isAdmin()&&!['households','overview','objects','device','live','playback','family','heritage','heritage-records','settings'].includes(page))return; if(['heritage','family','heritage-records'].includes(page)&¤tHome()?.kind&¤tHome().kind!=='home')return; if(['objects','overview','sites','users','family','heritage','heritage-records','households','storage','settings'].includes(page)&&typeof deviceContext!=='undefined')deviceContext=null; if(typeof scopeEntered!=='undefined')scopeEntered=!['households','storage','settings'].includes(page); if(page==='storage'&&!isPlatformAdmin())return; if(page!=='heritage-records'&&typeof resetHeritage==='function')resetHeritage(); if (activePage !== page) {stopPlayers();if(typeof closePtz==='function')closePtz();} if(page!=='family'&&typeof closeFamilyFullscreen==='function')closeFamilyFullscreen(); activePage = page; $('.metrics').hidden=['family','users','households','heritage','heritage-records','settings'].includes(page); $('.filter-row').hidden=!['objects','sites','live','playback','cameras'].includes(page)||!!deviceContext; $$('.page').forEach(el => el.hidden = el.id !== 'page-' + page); $$('nav button').forEach(b => b.classList.toggle('active', b.dataset.page === page || b.dataset.page==='objects'&&['device','cameras','recorders','live','playback'].includes(page)||b.dataset.page==='heritage'&&['family','heritage-records'].includes(page))); $('#page-title').textContent = pageInfo[page][0]; $('#page-note').textContent = pageInfo[page][1]; if (page !== 'playback') resetPlayback(); render(); if(page==='playback')$('#archive-source').value=state.cameras.find(c=>c.id===$('#play-camera').value)?.archiveSource||'local'; if (page === 'storage') loadAudit(); if (page === 'family') loadFamily(!options.keepFamilyView); if (page === 'users') loadUsers(); if(page==='heritage-records')loadHeritage(); if(page==='settings'&&typeof loadSettings==='function')loadSettings(); } function render() { $('#metric-all').textContent = state.assets.length+state.recorders.length+(state.devices||[]).length; $('#metric-ready').textContent = state.cameras.filter(c=>c.ready).length; $('#metric-sites').textContent = state.sites.length; $('#metric-used').textContent = state.storage.usedGB == null ? '—' : state.storage.usedGB + ' GB'; $('#service-status').textContent = state.mediaOnline ? '视频服务已连接' : '视频服务未连接'; $('#service-status').classList.toggle('off', !state.mediaOnline); $('#version').textContent = state.version ? 'v' + state.version + ' / 数据存于本机' : '独立部署 / 数据自有'; const warning = state.storage.paused ? state.storage.reason + ',已暂停录像。' : ''; $('#banner').textContent = warning; $('#banner').hidden = !warning; setOptions($('#site-filter'), '' + state.sites.map(s=>``).join('')); if (activePage === 'live') renderLive(); if (activePage === 'cameras') renderCameras(); if (activePage === 'sites') renderSites(); if (activePage === 'objects') renderAssets(); if (activePage === 'recorders') renderRecorders(); if (activePage === 'playback') { setOptions($('#play-camera'), filtered().map(c=>``).join('')); drawTimeline(); } if (activePage === 'storage') renderStorage(); if(typeof renderHouseholds==='function')renderHouseholds(); applyPermissions(); if(activePage==='live'&&typeof wallSync==='function')wallSync(); } function empty(title, description, button) { return `

${esc(title)}

${esc(description)}

${button || ''}
`; } function renderLive() { const cameras = filtered(), quality = $('#quality').value; const key = (currentUser?.role||'')+':'+(state.familyId||'')+':'+ZhaoLivePlayer.key(cameras, quality); if (key === liveKey) { updateLiveStatus(cameras); return; } stopPlayers(); liveKey = key; $('#live-grid').innerHTML = cameras.length ? cameras.map(c=>`
⠿ 拖动
◎${c.enabled ? '正在等待画面' : '设备连接已停用'}
${esc(c.name)}

${esc(siteName(c.siteId)+' · '+(c.point || '安装位置待填写'))}

${c.recordingActive ? '● 正在录像' : c.ready ? '码流已接通' : c.enabled ? '等待接通' : '已停用'}
`).join('') : (isAdmin()?empty('先接入一台摄像头','建立空间档案,再把设备添加到对应位置。',''):empty('当前范围没有可查看的摄像头','请选择已授权空间,或联系管理员配置观看权限。')); for (const c of cameras.filter(c=>c.enabled)) { const video = $(`video[data-camera="${c.id}"]`); players.push(new ZhaoLivePlayer(video, video.parentElement, `/media/live/cam_${c.id}_${quality}/index.m3u8`)); } } function updateLiveStatus(cameras) { for (const c of cameras) { const card=$(`video[data-camera="${c.id}"]`)?.closest('.camera-card'); if (!card) continue; card.querySelector('.camera-meta strong').textContent=c.name; card.querySelector('.camera-meta p').textContent=siteName(c.siteId)+' · '+(c.point || '安装位置待填写'); const badge=card.querySelector('.badge'); badge.textContent=c.recordingActive?'● 正在录像':c.ready?'码流已接通':c.enabled?'等待接通':'已停用'; badge.classList.toggle('off',!c.ready); } } function renderCameras() { $('#scan-button').disabled=state.scan.running; $('#scan-button').textContent=state.scan.running?'正在探测…':'开始探测'; $('#scan-result').innerHTML=(state.scan.items||[]).map(s=>`
${esc(s.host)} · RTSP 服务响应
`).join('') || (state.scan.at ? '

本次没有发现响应 RTSP 554 端口的设备。

' : ''); const cameras=filtered(); $('#camera-list').innerHTML=cameras.length?`
${cameras.map(c=>``).join('')}
设备 / 位置所属空间地址码流状态录像
${esc(c.name)}${esc(c.point||'位置待完善')}${esc(siteName(c.siteId))}${esc(c.host)}:${c.port}${c.ready?'已接通':c.enabled?'未接通':'停用'}${c.recordingActive?'正在录像':c.record?'已配置,等待写入':'未开启'}
`:empty('尚无摄像头','可以先扫描当前局域网,或手动填写设备地址。'); } function renderSites() { const range=$('#site-filter').value?ZhaoDevices.descendants(state.sites,$('#site-filter').value):null;const sites=state.sites.filter(s=>!range||range.has(s.id)); $('#site-list').innerHTML=sites.length?sites.map(s=>`
${esc(s.category||kind(s.kind))}

${esc(s.name)}

${esc(siteName(s.id))}

所属
${esc(s.owner||'待填写')}
地区
${esc([s.province,s.city,s.district,s.street].filter(Boolean).join(' / ')||'待填写')}
小区 / 园区
${esc(s.community||'待填写')}
楼栋门牌
${esc([s.building,s.unit,s.floor,s.door].filter(Boolean).join(' / ')||'待填写')}
本级设备
${ZhaoDevices.catalog(state).filter(a=>a.siteId===s.id).length} 台 / ${state.cameras.filter(c=>c.siteId===s.id).length} 个镜头
详细地址
${esc(s.address||'待填写')}
`).join(''):empty('建立第一个空间','给区域起个名字,按需选择上级区域;类型和地址可稍后填写。',''); } function renderStorage() { const s=state.storage; if (!$('#storage-form').contains(document.activeElement)) for(const k of ['retentionDays','maxGB','reserveGB']) $('#storage-form').elements[k].value=s[k]??''; $('#storage-status').innerHTML=[['录像占用',(s.usedGB??'—')+' GB'],['磁盘可用',(s.freeGB??'—')+' GB'],['视频服务',state.mediaOnline?'运行中':'未连接'],['空间保护',s.paused?s.reason:'未触发'],['录像方式','主码流持续录像 / 分段保存'],['系统运行',Math.floor(state.uptime/3600)+' 小时 '+Math.floor(state.uptime%3600/60)+' 分钟']].map(([k,v])=>`
${esc(k)}
${esc(v)}
`).join(''); } async function loadAudit() { try {const rows=await api('/api/audit');$('#audit-list').innerHTML=rows.slice(0,8).map(r=>`
${new Date(r.at).toLocaleString()}${esc(r.action)} ${esc(r.name)}
`).join('')||'

尚无操作记录

';}catch(e){if(!e.stale)toast(e.message);} } async function refresh() {if(!loggedIn)return;const userId=currentUser?.id,generation=++refreshGeneration;try{const next=await api('/api/state');if(!loggedIn||currentUser?.id!==userId||generation!==refreshGeneration)return;if(next.user.id!==userId||next.user.personId!==currentUser.personId){clearPrivateView();loggedIn=true;}state=next;currentUser=state.user;render();}catch(e){if(e.stale||generation!==refreshGeneration||!loggedIn)return;$('#banner').textContent=e.message;$('#banner').hidden=false;}} async function auth() {const generation=++authGeneration;try{const s=await api('/api/session');if(generation!==authGeneration)return;if(currentUser&&(!s.authenticated||currentUser.id!==s.user?.id||currentUser.personId!==s.user?.personId))clearPrivateView();loggedIn=s.authenticated;currentUser=s.user;setup=s.setupRequired;$('#setup-code-label').hidden=!setup;$('#auth-form').elements.username.readOnly=setup;if(setup)$('#auth-form').elements.username.value='admin';$('#auth-title').textContent=setup?'建立你的家庭空间':'欢迎回来';$('#auth-note').textContent=setup?'使用部署时生成的初始化码,设置管理员密码。':'输入用户名和密码,进入家庭空间。';applyPermissions();if(!loggedIn&&!$('#auth-dialog').open)$('#auth-dialog').showModal();if(loggedIn){$('#auth-dialog').close();await refresh();}if(typeof onAppearanceSession==='function')onAppearanceSession(s.preferences);}catch(e){if(!e.stale&&generation===authGeneration)toast('无法连接管理服务:'+e.message);}} function openSite(id) {const f=$('#site-form');f.reset();f.elements.parentId.innerHTML=''+spaceOptions(id);f.querySelector('.form-error').textContent='';const item=state.sites.find(s=>s.id===id);if(item)for(const [k,v]of Object.entries(item))if(f.elements[k])f.elements[k].value=v;$('#site-dialog').showModal();} function openCamera(id, host='') {if(!state.assets.length){toast('请先建立摄像头对象,再配置镜头通道');openAsset();return;}if(!state.sites.length){toast('请先建立空间档案,再添加摄像头');openSite();return;}const f=$('#camera-form');f.reset();f.querySelector('.form-error').textContent='';f.elements.assetId.innerHTML=state.assets.map(a=>``).join('');f.elements.recorderId.innerHTML=''+state.recorders.map(r=>``).join('');f.elements.archiveRecorderId.innerHTML=''+state.recorders.map(r=>``).join('');$('#onvif-result').innerHTML='';f.elements.siteId.innerHTML=state.sites.map(s=>``).join('');const c=state.cameras.find(c=>c.id===id);if(c){for(const [k,v]of Object.entries(c)){const el=f.elements[k];if(!el)continue;if(el.type==='checkbox')el.checked=v;else el.value=v;}}else{f.elements.host.value=host;if($('#site-filter').value)f.elements.siteId.value=$('#site-filter').value;}$('#path-preset').value='custom';if(!c){if(state.recorders.length){f.elements.recorderId.value=state.recorders[0].id;}else{f.elements.sourceKind.value='direct';f.elements.archiveSource.value='local';}}syncEndpoint();$('#camera-dialog').showModal();} function formValues(f) {return Object.fromEntries(new FormData(f).entries());} function handleForm(id, endpoint, transform=x=>x, after=()=>{}) {$(`#${id}`).addEventListener('submit',async e=>{e.preventDefault();const f=e.currentTarget,b=f.querySelector('[type=submit]');b.disabled=true;f.querySelector('.form-error').textContent='';try{await api(endpoint,transform(formValues(f),f));f.closest('dialog')?.close();toast('已保存');await refresh();after();}catch(err){f.querySelector('.form-error').textContent=err.message;}finally{b.disabled=false;}});} handleForm('site-form','/api/sites'); handleForm('camera-form','/api/cameras',(d,f)=>({...d,port:Number(d.port),enabled:f.elements.enabled.checked,record:f.elements.record.checked})); handleForm('storage-form','/api/storage',d=>Object.fromEntries(Object.entries(d).map(([k,v])=>[k,Number(v)])),loadAudit); $('#auth-form').addEventListener('submit',async e=>{e.preventDefault();const f=e.currentTarget,b=f.querySelector('button');b.disabled=true;try{await api(setup?'/api/setup':'/api/login',formValues(f));f.reset();f.querySelector('.form-error').textContent='';await auth();}catch(err){f.querySelector('.form-error').textContent=err.message;}finally{b.disabled=false;}}); $('#auth-dialog').addEventListener('cancel',e=>e.preventDefault()); document.addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;if(b.dataset.page)showPage(b.dataset.page);if(b.dataset.close)$(`#${b.dataset.close}`).close();if(b.dataset.action==='new-camera'){openCamera();if(deviceContext?.source==='camera'){$('#camera-form').elements.assetId.value=deviceContext.id;syncEndpoint();}}if(b.dataset.action==='new-asset')openAsset();if(b.dataset.action==='new-recorder')openRecorder();if(b.dataset.editAsset)openAsset(b.dataset.editAsset);if(b.dataset.editRecorder)openRecorder(b.dataset.editRecorder);if(b.dataset.action==='new-site')openSite();if(b.dataset.editSite)openSite(b.dataset.editSite);if(b.dataset.editCamera)openCamera(b.dataset.editCamera);if(b.dataset.found)openCamera(null,b.dataset.found);if(b.dataset.playCamera){const c=state.cameras.find(c=>c.id===b.dataset.playCamera);if(c)enterDevice('camera',c.assetId,'playback');$('#play-camera').value=b.dataset.playCamera;$('#archive-source').value=state.cameras.find(c=>c.id===b.dataset.playCamera)?.archiveSource||'local';queryRecordings();}if(b.dataset.span)playAt(new Date(b.dataset.span));}); $('#logout').onclick=async()=>{await api('/api/logout',{});loggedIn=false;clearPrivateView();await auth();}; $('#refresh').onclick=()=>{players.forEach(p=>p.retryIfNeeded());refresh();};$('#site-filter').onchange=()=>{stopPlayers();resetPlayback();render();};$('#quality').onchange=()=>{stopPlayers();render();}; $('#scan-button').onclick=async()=>{try{await api('/api/discover',{network:$('#scan-network').value});await refresh();}catch(e){toast(e.message);}}; $('#path-preset').onchange=e=>{const presets={tplink:['/stream1','/stream2'],hikvision:['/Streaming/Channels/101','/Streaming/Channels/102'],dahua:['/cam/realmonitor?channel=1&subtype=0','/cam/realmonitor?channel=1&subtype=1']};const v=presets[e.target.value];if(v){$('#camera-form').elements.mainPath.value=v[0];$('#camera-form').elements.subPath.value=v[1];}}; window.addEventListener('beforeunload',stopPlayers); setInterval(()=>{$('#clock').textContent=new Date().toLocaleString('zh-CN',{hour12:false});},1000); setInterval(refresh,12000);document.addEventListener('DOMContentLoaded',async()=>{showPage('households');if(typeof loadBranding==='function')await loadBranding();await auth();},{once:true}); function renderAssets(){renderDevices();} function renderRecorders(){ const recorders=state.recorders.filter(r=>!$('#site-filter').value||r.siteId===$('#site-filter').value); $('#recorder-list').innerHTML=recorders.length?recorders.map(r=>`
录像机

${esc(r.name)}

所属空间
${esc(siteName(r.siteId))}
品牌型号
${esc([r.brand,r.model].filter(Boolean).join(' / ')||'待确认')}
设备地址
${esc(r.host)}:${r.port}
凭据
${r.passwordConfigured?'已配置':'尚未填写'}
关联镜头
${state.cameras.filter(c=>c.recorderId===r.id).length} 个
原有录像回放
待型号适配
`).join(''):empty('连接现有录像机','填写录像机账号后,在各镜头通道选择这台录像机。',''); } function openAsset(id){if(!state.sites.length){toast('请先建立空间档案');openSite();return;}const f=$('#asset-form');f.reset();f.querySelector('.form-error').textContent='';f.elements.siteId.innerHTML=state.sites.map(s=>``).join('');const a=state.assets.find(a=>a.id===id);if(!a&&$('#site-filter').value)f.elements.siteId.value=$('#site-filter').value;if(a)for(const [k,v]of Object.entries(a))if(f.elements[k])f.elements[k].value=v;responsibilityFields(f,a);$('#asset-dialog').showModal();} function openRecorder(id){if(!state.sites.length){toast('请先建立空间档案');openSite();return;}const f=$('#recorder-form');f.reset();f.querySelector('.form-error').textContent='';f.elements.siteId.innerHTML=state.sites.map(s=>``).join('');const r=state.recorders.find(a=>a.id===id);if(r)for(const [k,v]of Object.entries(r))if(f.elements[k])f.elements[k].value=v;responsibilityFields(f,r);$('#recorder-dialog').showModal();} function syncEndpoint(){const f=$('#camera-form'),isRecorder=f.elements.sourceKind.value==='recorder',r=state.recorders.find(r=>r.id===f.elements.recorderId.value);f.elements.recorderId.disabled=!isRecorder;for(const k of ['host','port','username','password'])f.elements[k].readOnly=isRecorder;if(isRecorder&&r){f.elements.host.value=r.host;f.elements.port.value=r.port;f.elements.username.value=r.username;f.elements.password.value='';}const a=state.assets.find(a=>a.id===f.elements.assetId.value);if(a){f.elements.siteId.value=a.siteId;f.elements.point.value=f.elements.point.value||a.point;}f.elements.siteId.disabled=true;} $('#camera-form').elements.sourceKind.onchange=syncEndpoint;$('#camera-form').elements.recorderId.onchange=syncEndpoint;$('#camera-form').elements.assetId.onchange=syncEndpoint; handleForm('asset-form','/api/assets',d=>({...deviceValues(d),lensCount:Number(d.lensCount)}));handleForm('recorder-form','/api/recorders',d=>({...deviceValues(d),port:Number(d.port)})); $('#onvif-read').onclick=async()=>{const f=$('#camera-form'),b=$('#onvif-read');if(f.elements.sourceKind.value!=='direct'){toast('读取镜头配置时请使用直接连接摄像头');return;}b.disabled=true;$('#onvif-result').textContent='正在读取…';try{const d=await api('/api/onvif',{id:f.elements.id.value,host:f.elements.host.value,port:80,username:f.elements.username.value,password:f.elements.password.value});const groups=Map.groupBy?Map.groupBy(d.profiles,p=>p.sourceToken):d.profiles.reduce((m,p)=>{const k=p.sourceToken;m.set(k,[...(m.get(k)||[]),p]);return m;},new Map());$('#onvif-result').innerHTML='';const note=document.createElement('p');note.className='description';note.textContent=[d.device.Manufacturer,d.device.Model].join(' · ')+' / '+groups.size+' 个视频源';$('#onvif-result').append(note);for(const [source,profiles]of groups){profiles.sort((a,b)=>Number(b.width)*Number(b.height)-Number(a.width)*Number(a.height));const btn=document.createElement('button');btn.type='button';btn.className='quiet';btn.textContent=source+' · '+profiles[0].width+' × '+profiles[0].height;btn.onclick=()=>{f.elements.mainPath.value=profiles[0].path;f.elements.subPath.value=(profiles[1]||profiles[0]).path;f.elements.port.value=profiles[0].port;f.elements.model.value=d.device.Model;toast('已填入该视频源的主、子码流路径,请保存');};$('#onvif-result').append(btn);}}catch(e){$('#onvif-result').textContent=e.message;}finally{b.disabled=false;}}; function validateNewPassword(values){ if(values.newPassword!==values.confirmPassword)throw Error('两次输入的新密码不一致'); if(values.newPassword===values.currentPassword)throw Error('新密码不能与当前密码相同'); if(values.newPassword.length<8||values.newPassword.length>128)throw Error('密码需要 8 至 128 个字符'); } $('#account-password').onclick=()=>{if(!loggedIn)return;const f=$('#password-form');f.reset();f.querySelector('.form-error').textContent='';$('#password-account').textContent='当前账号:'+currentUser.username;$('#password-dialog').showModal();}; $('#password-form').addEventListener('submit',async e=>{ e.preventDefault();const f=e.currentTarget,b=f.querySelector('[type=submit]');b.disabled=true; try{const data=formValues(f);validateNewPassword(data);await api('/api/account/password',{currentPassword:data.currentPassword,newPassword:data.newPassword});clearPrivateView();await auth();toast('密码已修改,请用新密码重新登录');} catch(err){if(!err.stale)f.querySelector('.form-error').textContent=err.message;}finally{b.disabled=false;} });