2026-10-04 10:43:17 +08:00
|
|
|
'use strict';
|
2026-10-04 16:21:40 +08:00
|
|
|
pageInfo.households=['我的家庭与场所','家庭、办公室或工厂,分别组织,各自授权。'];
|
2026-10-04 11:19:14 +08:00
|
|
|
const scopeKind=h=>({home:'家庭',office:'办公场所',factory:'工厂'}[h.kind||'home']||'家庭');
|
2026-10-04 21:30:30 +08:00
|
|
|
let coverSaving=false;
|
2026-10-05 19:07:19 +08:00
|
|
|
let homeLayoutDraft=null,homeLayoutOwner='',homeLayoutDirty=false,homeLayoutSaving=false,homePage=0,homeArrange=false,homeDragId='';
|
|
|
|
|
let homePresentation='cards';
|
|
|
|
|
function homeOrder(homes,order){const byId=new Map(homes.map(h=>[h.id,h]));return [...new Set([...order,...homes.map(h=>h.id)])].filter(id=>byId.has(id)).map(id=>byId.get(id));}
|
|
|
|
|
function homeMove(id,step,target=''){
|
|
|
|
|
if(homeLayoutSaving)return;
|
|
|
|
|
const order=homeOrder(state.households||[],homeLayoutDraft.order).map(h=>h.id),from=order.indexOf(id),to=target?order.indexOf(target):from+step;
|
|
|
|
|
if(from<0||to<0||to>=order.length||from===to)return;
|
|
|
|
|
order.splice(from,1);order.splice(to,0,id);homeLayoutDraft.order=order;homeLayoutDirty=true;homePage=Math.floor(to/(homeLayoutDraft.columns*homeLayoutDraft.rows));renderHouseholds();
|
|
|
|
|
}
|
|
|
|
|
function homeArrangeActions(id,homes){const index=homes.findIndex(h=>h.id===id);return homeArrange?`<div class="home-order-actions"><span>拖动卡片或移动顺序</span><button class="quiet" data-home-move="${esc(id)}" data-step="-1" aria-label="${esc(homes[index].name)}前移" ${index===0?'disabled':''}>← 前移</button><button class="quiet" data-home-move="${esc(id)}" data-step="1" aria-label="${esc(homes[index].name)}后移" ${index===homes.length-1?'disabled':''}>后移 →</button></div>`:'';}
|
2026-10-04 10:43:17 +08:00
|
|
|
function renderHouseholds(){
|
2026-10-05 19:07:19 +08:00
|
|
|
const saved=state.householdLayout||{revision:0,columns:2,rows:2,groupCity:false,order:[]};
|
|
|
|
|
if(homeLayoutOwner!==currentUser?.id){homeLayoutOwner=currentUser?.id||'';homeLayoutDirty=false;homeLayoutDraft=null;homePage=0;homeArrange=false;}
|
|
|
|
|
if(!homeLayoutDraft||!homeLayoutDirty&&!homeLayoutSaving&&homeLayoutDraft.revision!==saved.revision)homeLayoutDraft={...saved,order:[...saved.order]};
|
|
|
|
|
const homes=homeOrder(state.households||[],homeLayoutDraft.order);
|
2026-10-04 11:19:14 +08:00
|
|
|
setOptions($('#household-filter'),homes.map(h=>`<option value="${esc(h.id)}">${esc(scopeKind(h)+' · '+h.name)}</option>`).join(''));
|
2026-10-04 10:43:17 +08:00
|
|
|
$('#household-filter').value=state.familyId||'';
|
|
|
|
|
if(activePage!=='households')return;
|
2026-10-05 19:07:19 +08:00
|
|
|
$('#home-columns').value=homeLayoutDraft.columns;$('#home-rows').value=homeLayoutDraft.rows;$('#home-group-city').checked=homeLayoutDraft.groupCity;
|
|
|
|
|
if(homeLayoutDraft.groupCity)homeArrange=false;
|
|
|
|
|
$('#home-arrange').checked=homeArrange;$('#home-arrange').disabled=homeLayoutDraft.groupCity||homeLayoutSaving;
|
|
|
|
|
$('#home-layout-save').disabled=!homeLayoutDirty||homeLayoutSaving;$('#home-layout-reset').disabled=!homeLayoutDirty||homeLayoutSaving;
|
|
|
|
|
for(const id of ['home-columns','home-rows','home-group-city'])$('#'+id).disabled=homeLayoutSaving;
|
|
|
|
|
$('#home-layout-status').textContent=homeLayoutSaving?'正在保存布局…':homeLayoutDirty?'布局已调整,点击“保存我的布局”后下次打开会沿用。':homeLayoutDraft.groupCity?'按城市分组展示;取消分组后可自由排列卡片。':'可设置每页列数和行数;手机端自动收为单列。布局仅影响当前账号。';
|
|
|
|
|
const capacity=homeLayoutDraft.columns*homeLayoutDraft.rows,pages=Math.max(1,Math.ceil(homes.length/capacity));homePage=Math.min(homePage,pages-1);
|
|
|
|
|
const visible=homes.slice(homePage*capacity,(homePage+1)*capacity);
|
|
|
|
|
$('#home-prev').disabled=homePage===0;$('#home-next').disabled=homePage>=pages-1;$('#home-page-info').textContent=`第 ${homePage+1} / ${pages} 页 · 共 ${homes.length} 个 · 每页最多 ${capacity} 张`;
|
|
|
|
|
$('#household-list').style.setProperty('--home-columns',homeLayoutDraft.columns);
|
|
|
|
|
const cards=h=>`<article class="site-card home-card" data-home-id="${esc(h.id)}"><div class="home-cover ${h.coverUrl?'has-photo':''}">${h.coverUrl?`<img src="${esc(h.coverUrl)}" alt="${esc(h.name)}封面照片" loading="lazy">`:'<div class="home-cover-placeholder" aria-hidden="true">山川有归处 · 灯火有人家</div>'}<div class="home-cover-content"><div class="site-heading"><span class="badge">${h.id===state.familyId?'当前归属 · ':''}${scopeKind(h)}</span>${isAdmin()?`<button class="quiet" data-edit-household="${esc(h.id)}">编辑档案</button>`:''}</div><h3>${esc(h.name)}</h3><p class="home-location">${esc(ZhaoGeography.describe(h)||'地区待填写')}</p></div></div><div class="home-card-body">${homeArrangeActions(h.id,homes)}<p class="description">${esc(h.note||'区域与设备归属独立管理')}</p><dl><div><dt>${!h.kind||h.kind==='home'?'一家之主':'主要负责人'}</dt><dd>${esc(h.ownerName||'待指定')}</dd></div><div><dt>备用负责人</dt><dd>${esc(h.successorName||'待指定')}</dd></div><div><dt>账号 / 空间</dt><dd>${h.members} 个 / ${h.sites} 处</dd></div></dl>${!h.successorId?'<p class="description">尚未指定备用负责人,建议提前安排可信人员接管。</p>':''}<div class="household-actions"><button class="secondary" data-enter-household="${esc(h.id)}">进入${esc(h.name)}</button>${h.canAssignOwner?`<button class="quiet" data-leadership="${esc(h.id)}">负责人安排与交接</button>`:''}</div>${h.leadershipHistory?.length?`<details><summary>交接记录</summary>${h.leadershipHistory.slice().reverse().map(r=>`<p class="description">${esc(new Date(r.at).toLocaleString())} · ${esc(r.actor)}<br>${esc(r.reason)}</p>`).join('')}</details>`:''}</div></article>`;
|
|
|
|
|
const groups=new Map();for(const h of visible){const key=[h.geography?.body&&h.geography.body!=='地球'?h.geography.body:'',h.geography?.country||h.country||'',h.province,h.city||'未填写城市/区域'].filter(Boolean).join(' / ');if(!groups.has(key))groups.set(key,[]);groups.get(key).push(h);}
|
|
|
|
|
$('#household-list').innerHTML=homeLayoutDraft.groupCity?[...groups].map(([city,rows])=>`<section class="home-city-group"><h3>${esc(city)}</h3><div class="home-card-grid">${rows.map(cards).join('')}</div></section>`).join(''):`<div class="home-card-grid">${visible.map(cards).join('')}</div>`;
|
|
|
|
|
$('#household-list').hidden=homePresentation!=='cards';$('#home-global-topology').hidden=homePresentation!=='topology';$('.home-layout-controls').hidden=homePresentation!=='cards';$('.home-pagination').hidden=homePresentation!=='cards';
|
|
|
|
|
$('#home-card-view').setAttribute('aria-pressed',String(homePresentation==='cards'));$('#home-topology-view').setAttribute('aria-pressed',String(homePresentation==='topology'));
|
|
|
|
|
if(homePresentation==='topology')$('#home-global-topology').innerHTML=`<div class="platform-topology-root"><h3>${esc(typeof branding==='object'?branding.name:'赵府')} · 数字管家</h3><p>${homes.length} 个已授权家庭/场所 · 点击进入内部拓扑</p></div><p class="description">此图表示平台与场所的管理关系;网络标签来自设备档案,连线不代表物理专线或实时在线状态。</p><div class="platform-place-grid">${homes.map(h=>`<button class="platform-place-node" data-home-network="${esc(h.id)}"><span class="badge">${esc(scopeKind(h))}</span><h3>${esc(h.name)}</h3><p>${esc(ZhaoGeography.describe(h)||'地址待填写')}</p><p>${h.deviceCount||0} 台已授权设备</p><small>${(h.networkAccesses||[]).map(x=>esc(networkLabels[x]||'接入线路待确认')).join(' · ')||'尚未登记网络设备'}</small><span>查看场所网络拓扑 →</span></button>`).join('')}</div>`;
|
|
|
|
|
for(const card of $$('#household-list .home-card')){card.draggable=homeArrange&&!homeLayoutSaving;card.ondragstart=e=>{if(e.target.closest('button,a,input,select')){e.preventDefault();return;}homeDragId=card.dataset.homeId;e.dataTransfer.setData('text/plain',homeDragId);};card.ondragover=e=>{if(homeArrange&&homeDragId)e.preventDefault();};card.ondrop=e=>{e.preventDefault();if(homeArrange&&homeDragId)homeMove(homeDragId,0,card.dataset.homeId);homeDragId='';};card.ondragend=()=>{homeDragId='';};}
|
2026-10-04 10:43:17 +08:00
|
|
|
}
|
2026-10-05 19:07:19 +08:00
|
|
|
for(const [id,key] of [['home-columns','columns'],['home-rows','rows'],['home-group-city','groupCity']])$('#'+id).onchange=e=>{homeLayoutDraft[key]=key==='groupCity'?e.target.checked:Number(e.target.value);homePage=0;homeLayoutDirty=true;renderHouseholds();};
|
|
|
|
|
$('#home-arrange').onchange=e=>{homeArrange=e.target.checked;renderHouseholds();};
|
|
|
|
|
$('#home-prev').onclick=()=>{homePage=Math.max(0,homePage-1);renderHouseholds();};$('#home-next').onclick=()=>{homePage++;renderHouseholds();};
|
|
|
|
|
$('#home-card-view').onclick=()=>{homePresentation='cards';renderHouseholds();};$('#home-topology-view').onclick=()=>{homePresentation='topology';renderHouseholds();};
|
|
|
|
|
$('#home-global-topology').addEventListener('click',async e=>{const b=e.target.closest('[data-home-network]');if(!b)return;if(b.dataset.homeNetwork!==state.familyId&&!await switchHousehold(b.dataset.homeNetwork))return;devicePresentation='topology';showPage('objects');});
|
|
|
|
|
$('#home-layout-reset').onclick=()=>{homeLayoutDirty=false;homeLayoutDraft=null;homePage=0;renderHouseholds();};
|
|
|
|
|
$('#household-list').addEventListener('click',e=>{const button=e.target.closest('[data-home-move]');if(button)homeMove(button.dataset.homeMove,Number(button.dataset.step));});
|
|
|
|
|
$('#home-layout-save').onclick=async()=>{if(homeLayoutSaving||!homeLayoutDirty)return;const generation=sessionGeneration;homeLayoutSaving=true;renderHouseholds();try{const saved=await api('/api/households/layout',homeLayoutDraft);if(generation!==sessionGeneration)return;state.householdLayout=saved;homeLayoutDraft={...saved,order:[...saved.order]};homeLayoutDirty=false;toast('卡片布局与顺序已保存');}catch(e){if(!e.stale)toast(e.message);}finally{homeLayoutSaving=false;if(generation===sessionGeneration)renderHouseholds();}};
|
2026-10-04 10:43:17 +08:00
|
|
|
async function switchHousehold(id){
|
2026-10-04 20:37:14 +08:00
|
|
|
if(id===state.familyId)return true;
|
|
|
|
|
if(!isPlatformAdmin()||!id)return false;
|
|
|
|
|
if(typeof confirmJournalDiscard==='function'&&!confirmJournalDiscard())return false;
|
2026-10-04 10:43:17 +08:00
|
|
|
const user=currentUser;
|
|
|
|
|
// Invalidate outstanding API responses, video players and dialogs before changing scope.
|
|
|
|
|
clearPrivateView();
|
|
|
|
|
householdScope=id;currentUser=user;loggedIn=true;
|
2026-10-04 20:37:14 +08:00
|
|
|
await auth();return loggedIn;
|
2026-10-04 10:43:17 +08:00
|
|
|
}
|
|
|
|
|
function openHousehold(id){
|
2026-10-04 21:30:30 +08:00
|
|
|
if(coverSaving){toast('档案仍在保存,请稍候。');return;}
|
2026-10-04 10:43:17 +08:00
|
|
|
if(!isAdmin())return;
|
|
|
|
|
const item=(state.households||[]).find(h=>h.id===id);
|
|
|
|
|
if(!item&&!isPlatformAdmin())return;
|
2026-10-04 21:30:30 +08:00
|
|
|
nextImageJob('cover');$('#household-cover-status').dataset.error='false';fillForm($('#household-form'),{profileRevision:0,...(item||{})});coverDraft=undefined;ZhaoGeography.fill($('#household-form'),item||{});$('#household-cover-preview').hidden=!item?.coverUrl;if(item?.coverUrl)$('#household-cover-preview').src=item.coverUrl;else $('#household-cover-preview').removeAttribute('src');$('#household-cover-status').textContent='封面存入百度网盘,原图可选 30 MB 以内;浏览器会先压缩。仅向获授权账号开放。';$('#household-dialog').showModal();
|
2026-10-04 10:43:17 +08:00
|
|
|
}
|
|
|
|
|
async function openLeadership(id){
|
|
|
|
|
try{
|
2026-10-04 20:37:14 +08:00
|
|
|
if(id!==state.familyId&&!await switchHousehold(id))return;
|
2026-10-04 10:43:17 +08:00
|
|
|
const home=(state.households||[]).find(h=>h.id===id);
|
|
|
|
|
if(!home?.canAssignOwner)return;
|
|
|
|
|
const result=await api('/api/users');
|
|
|
|
|
const candidates=result.users.filter(u=>u.role==='family_admin'&&!u.disabled);
|
|
|
|
|
const f=$('#leadership-form'),options=candidates.map(u=>`<option value="${esc(u.id)}">${esc(u.name+' / '+u.username)}</option>`).join('');
|
2026-10-04 11:19:14 +08:00
|
|
|
f.elements.ownerId.innerHTML='<option value="">请选择主要负责人</option>'+options;
|
2026-10-04 10:43:17 +08:00
|
|
|
f.elements.successorId.innerHTML='<option value="">暂未指定</option>'+options;
|
|
|
|
|
fillForm(f,{id:home.id,revision:home.leadershipRevision||0,ownerId:home.ownerId||'',successorId:home.successorId||''});
|
|
|
|
|
$('#leadership-dialog').showModal();
|
|
|
|
|
}catch(error){if(!error.stale)toast(error.message);}
|
|
|
|
|
}
|
|
|
|
|
$('#leadership-form').addEventListener('submit',async e=>{
|
|
|
|
|
e.preventDefault();const f=e.currentTarget,b=f.querySelector('[type=submit]');b.disabled=true;
|
2026-10-04 11:19:14 +08:00
|
|
|
try{const d=formValues(f);d.revision=Number(d.revision);await api('/api/households/leadership',d);f.closest('dialog').close();await refresh();showPage('households');toast('负责人已更新,交接记录已保存');}
|
2026-10-04 10:43:17 +08:00
|
|
|
catch(error){if(!error.stale)f.querySelector('.form-error').textContent=error.message;}finally{b.disabled=false;}
|
|
|
|
|
});
|
2026-10-04 20:37:14 +08:00
|
|
|
$('#household-filter').onchange=async e=>{const switched=await switchHousehold(e.target.value);if(switched&&loggedIn)showPage('overview');else e.target.value=state.familyId;};
|
2026-10-04 10:43:17 +08:00
|
|
|
$('#new-household').onclick=()=>openHousehold();
|
|
|
|
|
$('#household-form').addEventListener('submit',async e=>{
|
2026-10-04 21:30:30 +08:00
|
|
|
e.preventDefault();const f=e.currentTarget;if(coverSaving)return;
|
|
|
|
|
if(imageJobPending('cover')){f.querySelector('.form-error').textContent='照片仍在处理,请完成后保存。';return;}
|
|
|
|
|
if(imageJobs.get('cover')?.error){f.querySelector('.form-error').textContent='封面未准备成功:请重新选择图片,或点击“取消本次图片选择”。';return;}
|
|
|
|
|
const payload={...formValues(f),profileRevision:Number(f.elements.profileRevision.value||0),...(coverDraft!==undefined?{coverData:coverDraft}:{})};
|
|
|
|
|
const controls=[...f.elements],disabled=controls.map(x=>x.disabled),generation=sessionGeneration;
|
|
|
|
|
coverSaving=true;controls.forEach(x=>x.disabled=true);f.querySelector('.form-error').textContent=payload.coverData?'正在上传封面到百度网盘并校验,请稍候…':'正在保存档案…';
|
|
|
|
|
try{
|
|
|
|
|
const saved=await api('/api/households',payload);if(generation!==sessionGeneration)return;
|
|
|
|
|
if(payload.coverData&&!saved.coverRef?.fsId)throw Error('服务器未确认封面保存,请重试。');
|
|
|
|
|
// Adopt the returned revision even if a later state refresh fails.
|
|
|
|
|
f.elements.id.value=saved.id;f.elements.profileRevision.value=saved.profileRevision;
|
|
|
|
|
const view={...saved,coverUrl:saved.coverData||saved.coverRef?'/api/households/cover?id='+encodeURIComponent(saved.id)+'&v='+saved.coverRevision:''};
|
|
|
|
|
delete view.coverData;delete view.coverRef;const previous=(state.households||[]).find(x=>x.id===saved.id);
|
|
|
|
|
if(previous)Object.assign(previous,view);else (state.households||=[]).push(view);
|
2026-10-04 22:10:03 +08:00
|
|
|
renderHouseholds();f.closest('dialog').close();toast(payload.coverData?'封面已存百度网盘,归属档案已保存':payload.coverData===''?'归属档案已保存,已使用默认封面':'归属档案已保存');
|
|
|
|
|
try{await refresh();}catch(error){if(!error.stale&&generation===sessionGeneration)toast('档案已保存,列表刷新暂未完成,请稍后刷新页面。无需重复保存。');}
|
2026-10-04 21:30:30 +08:00
|
|
|
}catch(error){if(!error.stale&&generation===sessionGeneration)f.querySelector('.form-error').textContent=error.message;}
|
|
|
|
|
finally{coverSaving=false;controls.forEach((x,i)=>x.disabled=disabled[i]);}
|
2026-10-04 10:43:17 +08:00
|
|
|
});
|
|
|
|
|
document.addEventListener('click',async e=>{
|
|
|
|
|
const b=e.target.closest('button');if(!b)return;
|
|
|
|
|
if(b.dataset.editHousehold)openHousehold(b.dataset.editHousehold);
|
|
|
|
|
if(b.dataset.leadership)await openLeadership(b.dataset.leadership);
|
2026-10-04 20:37:14 +08:00
|
|
|
if(b.dataset.enterHousehold){const switched=await switchHousehold(b.dataset.enterHousehold);if(switched&&loggedIn)showPage('overview');}
|
2026-10-04 10:43:17 +08:00
|
|
|
});
|
2026-10-04 17:22:42 +08:00
|
|
|
|
2026-10-04 21:30:30 +08:00
|
|
|
$('#household-cover-file').onchange=async e=>{
|
|
|
|
|
const file=e.target.files[0];if(!file)return;
|
|
|
|
|
const job=nextImageJob('cover'),generation=sessionGeneration,status=$('#household-cover-status');
|
|
|
|
|
imageJobs.get('cover').pending=true;status.dataset.error='false';status.textContent='正在压缩照片,请稍候…';$('#household-form .form-error').textContent='';
|
|
|
|
|
try{const image=await readImage(file,960);if(generation!==sessionGeneration||!imageJobCurrent('cover',job)||!$('#household-dialog').open)return;
|
|
|
|
|
coverDraft=image;$('#household-cover-preview').src=image;$('#household-cover-preview').hidden=false;status.textContent='封面预览已就绪(约 '+Math.ceil((image.length-22)*.75/1024)+' KB),保存后上传百度网盘并更新卡片。';
|
|
|
|
|
}catch(error){if(generation===sessionGeneration&&imageJobCurrent('cover',job)){imageJobs.get('cover').error=true;status.dataset.error='true';status.textContent=error.message+'。本次图片未保存,请重新选择或取消本次选择。';}}
|
|
|
|
|
finally{if(imageJobCurrent('cover',job))imageJobs.get('cover').pending=false;}
|
|
|
|
|
};
|
|
|
|
|
$('#household-cover-cancel').onclick=()=>{
|
|
|
|
|
nextImageJob('cover');coverDraft=undefined;$('#household-cover-file').value='';
|
|
|
|
|
const item=(state.households||[]).find(x=>x.id===$('#household-form').elements.id.value),img=$('#household-cover-preview');
|
|
|
|
|
img.hidden=!item?.coverUrl;if(item?.coverUrl)img.src=item.coverUrl;else img.removeAttribute('src');
|
|
|
|
|
$('#household-cover-status').dataset.error='false';$('#household-cover-status').textContent='已取消本次图片选择,保留原有封面。';$('#household-form .form-error').textContent='';
|
|
|
|
|
};
|
|
|
|
|
$('#household-cover-reset').onclick=()=>{nextImageJob('cover');coverDraft='';$('#household-cover-file').value='';$('#household-cover-preview').hidden=true;$('#household-cover-preview').removeAttribute('src');$('#household-cover-status').dataset.error='false';$('#household-cover-status').textContent='保存后使用默认封面。';$('#household-form .form-error').textContent='';};
|
2026-10-04 19:22:31 +08:00
|
|
|
$('#household-dialog').addEventListener('close',()=>nextImageJob('cover'));
|
2026-10-04 21:30:30 +08:00
|
|
|
$('#household-dialog').addEventListener('cancel',event=>{if(coverSaving)event.preventDefault();});
|
|
|
|
|
$('#household-cover-preview').addEventListener('error',()=>{$('#household-cover-status').textContent='封面暂时无法从网盘读取,请稍后重新打开;原有记录仍保留。';});
|