feat: isolate households and add explicit ownership succession

This commit is contained in:
Codex
2026-10-04 10:43:17 +08:00
parent a3f4544ddf
commit 72a3252e25
15 changed files with 635 additions and 60 deletions
+20 -8
View File
@@ -6,22 +6,32 @@ const kind = (v) => ({home:'家庭',company:'公司',other:'其他'}[v] || '其
let state = {cameras:[], assets:[], recorders:[], sites:[], storage:{}, scan:{}}, activePage = 'live', 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;
const isAdmin = () => currentUser?.role === 'admin';
let householdScope = '';
const isPlatformAdmin = () => currentUser?.role === 'admin';
const isAdmin = () => ['admin','family_admin'].includes(currentUser?.role);
const roleName = user => user.role==='admin'?'超级管理员':user.role==='family_admin'?((state.households||[]).some(h=>h.ownerId===user.id)?'一家之主':'家庭管理员'):'家庭成员';
function applyPermissions(){
const admin=isAdmin();
$('nav [data-page="storage"]').hidden=!isPlatformAdmin();
$('#account-password').hidden=!loggedIn;
$$('[data-admin],nav [data-page="objects"],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();
$('#scan-result').hidden=!isPlatformAdmin();
$('#new-household').hidden=!isPlatformAdmin();
$('#household-context').hidden=!loggedIn;
$('#household-filter').disabled=!isPlatformAdmin();
$('nav [data-page="family"]').hidden=!admin&&(!currentUser||currentUser.familyAccess==='none');
$('#current-user').textContent=currentUser ? currentUser.name+' · '+(admin?'管理员':'个人账号') : '';
$('#current-user').textContent=currentUser ? currentUser.name+' · '+roleName(currentUser) : '';
}
function clearPrivateView(){
if(typeof clearSnapshot==='function')clearSnapshot();
if(typeof clearCameraSettings==='function')clearCameraSettings();
sessionGeneration++;refreshGeneration++;authGeneration++;loggedIn=false;
if(typeof closePtz==='function')closePtz();if(document.fullscreenElement)document.exitFullscreen().catch(()=>{});
currentUser=null;stopPlayers();resetPlayback();
currentUser=null;householdScope='';stopPlayers();resetPlayback();
state={cameras:[],assets:[],recorders:[],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'])$('#'+id).replaceChildren();
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());
@@ -30,7 +40,7 @@ function clearPrivateView(){
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'}, body:data === undefined ? undefined : JSON.stringify(data)});
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}); }
@@ -44,11 +54,12 @@ function setOptions(select, html) { const old = select.value; select.innerHTML =
function stopPlayers() { players.forEach(p => p.destroy()); players = []; $$('#live-grid video').forEach(v => {v.pause();v.removeAttribute('src');v.load();}); liveKey = ''; }
function showPage(page) {
if(!isAdmin()&&!['live','playback','family'].includes(page))return;
if(page==='storage'&&!isPlatformAdmin())return;
if (activePage !== page) {stopPlayers();if(typeof closePtz==='function')closePtz();}
if(page!=='family'&&typeof closeFamilyFullscreen==='function')closeFamilyFullscreen();
activePage = page;
$('.metrics').hidden=['family','users'].includes(page);
$('.filter-row').hidden=['family','users'].includes(page);
$('.metrics').hidden=['family','users','households'].includes(page);
$('.filter-row').hidden=['family','users','households'].includes(page);
$$('.page').forEach(el => el.hidden = el.id !== 'page-' + page);
$$('nav button').forEach(b => b.classList.toggle('active', b.dataset.page === page));
$('#page-title').textContent = pageInfo[page][0]; $('#page-note').textContent = pageInfo[page][1];
@@ -79,13 +90,14 @@ function render() {
drawTimeline();
}
if (activePage === 'storage') renderStorage();
if(typeof renderHouseholds==='function')renderHouseholds();
applyPermissions();
if(activePage==='live'&&typeof wallSync==='function')wallSync();
}
function empty(title, description, button) { return `<div class="empty"><h3>${esc(title)}</h3><p>${esc(description)}</p>${button || ''}</div>`; }
function renderLive() {
const cameras = filtered(), quality = $('#quality').value;
const key = ZhaoLivePlayer.key(cameras, quality);
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=>`<article class="camera-card"><div class="wall-reorder"><span draggable="false" data-wall-drag="${c.id}" title="拖动排列">⠿ 拖动</span><button class="quiet" data-wall-before="${c.id}" aria-label="${esc(c.name)}前移">前移</button><button class="quiet" data-wall-after="${c.id}" aria-label="${esc(c.name)}后移">后移</button></div><div class="video-panel"><video data-camera="${c.id}" muted playsinline controls></video><div class="video-empty"><span class="lens">◎</span><span>${c.enabled ? '正在等待画面' : '设备连接已停用'}</span></div><div class="video-error" hidden></div></div><div class="camera-meta"><div><strong>${esc(c.name)}</strong><p>${esc(siteName(c.siteId)+' · '+(c.point || '安装位置待填写'))}</p></div><span class="badge ${c.ready?'':'off'}">${c.recordingActive ? '● 正在录像' : c.ready ? '码流已接通' : c.enabled ? '等待接通' : '已停用'}</span></div><div class="camera-actions"><button class="quiet" data-edit-camera="${c.id}">设备配置</button><button class="quiet" data-camera-settings="${c.id}">摄像机设置</button><button class="quiet" data-play-camera="${c.id}">查看录像</button><button class="quiet" data-snapshot-camera="${c.id}">截图</button><button class="quiet" data-ptz-camera="${c.id}" hidden>云台控制</button></div></article>`).join('') : (isAdmin()?empty('先接入一台摄像头','建立空间档案,再把设备添加到对应位置。','<button data-action="new-camera">+ 添加摄像头</button>'):empty('当前范围没有可查看的摄像头','请选择已授权空间,或联系管理员配置观看权限。'));