'use strict'; pageInfo.family=['家谱','记下亲人的故事,让世代之间有迹可循。']; pageInfo.users=['账号与关系','每个人有自己的账号,权限与亲属关系分别设置。']; let family={people:[],links:[],revision:0,canEdit:false}, users=[], focusPerson='',familyGeneration=0; const relationships=['本人','配偶','哥哥','弟弟','姐姐','妹妹','爸爸','妈妈','爷爷','奶奶','姥爷','姥姥','伯伯','叔叔','姑姑','舅舅','姨妈','儿子','女儿','孙子','孙女','外孙','外孙女','曾孙','曾孙女','玄孙','玄孙女','祖先','后裔','亲友','同事']; $('#relationship-choices').innerHTML=relationships.map(r=>``).join(''); function clearFamilyState(){familyGeneration++;family={people:[],links:[],revision:0,canEdit:false};users=[];focusPerson='';$('#family-reference').replaceChildren();delete $('#family-reference').dataset.initialized;$('#family-search').value='';$('#family-archived').checked=false;} function personName(id){return family.people.find(p=>p.id===id)?.name||'未关联';} function peopleOptions(blank=true){return (blank?'':'')+family.people.map(p=>``).join('');} function fillForm(form,data){form.reset();form.querySelector('.form-error').textContent='';for(const [k,v]of Object.entries(data)){const e=form.elements[k];if(!e)continue;if(e.type==='checkbox')e.checked=!!v;else e.value=v??'';}} async function loadFamily(){ const generation=++familyGeneration; try{const result=await api('/api/family');if(!loggedIn||generation!==familyGeneration)return;family=result; if(!family.people.some(p=>p.id===focusPerson))focusPerson=family.selfId||family.people.find(p=>!p.archived)?.id||''; setOptions($('#family-reference'),peopleOptions(false));if(!$('#family-reference').value&&family.selfId)$('#family-reference').value=family.selfId; if(!$('#family-reference').dataset.initialized&&family.selfId){$('#family-reference').value=family.selfId;$('#family-reference').dataset.initialized='true';} $('#family-error').textContent='';renderFamily(); }catch(e){$('#family-error').textContent=e.message;} } function relationText(e){return personName(e.fromId)+(e.kind==='parent'?' → 子女:':' ↔ 配偶:')+personName(e.toId)+(e.active?'':'(已停用)');} function renderFamily(){ const reference=$('#family-reference').value,term=$('#family-search').value.trim().toLowerCase(); const people=family.people.filter(p=>($('#family-archived').checked||!p.archived)&&[p.name,p.alias,p.birthplace].join(' ').toLowerCase().includes(term)); $('#family-count').textContent=family.people.filter(p=>!p.archived).length+' 位人物 · '+family.links.filter(e=>e.active).length+' 条关系'; $('#new-person').hidden=!family.canEdit; $('#family-people').innerHTML=people.map(p=>``).join('')||empty('从一个人物开始','可先录入自己,再逐步补齐父母、祖先和后辈。'); const p=family.people.find(p=>p.id===focusPerson); if(!p){$('#family-detail').innerHTML='

人物档案

选择或录入一位人物,查看关系与家谱。

';$('#family-tree').replaceChildren();return;} const edges=family.links.filter(e=>e.fromId===p.id||e.toId===p.id); $('#family-detail').innerHTML=`
人物档案

${esc(p.name)}

${family.canEdit?``:''}

相对于 ${esc(personName(reference))}:${esc(ZhaoKinship.describe(family.people,family.links,reference,p.id))}

${[['别名',p.alias],['性别',{male:'男',female:'女',unknown:'待确认'}[p.gender]],['在世情况',{alive:'在世',deceased:'已故',unknown:'待确认'}[p.lifeStatus]],['出生日期',ZhaoCalendar.describe(p.birthDate)],...(p.lifeStatus==='deceased'?[['去世日期',ZhaoCalendar.describe(p.deathDate)]]:[]),['籍贯 / 出生地',p.birthplace]].map(([k,v])=>`
${k}
${esc(v||'待补充')}
`).join('')}

生平与记载

${esc(p.biography||'尚未记录生平')}

${esc(p.note||'')}

亲属关系

${family.canEdit?'':''}
${edges.map(e=>``).join('')||'

尚未连接亲属。

'}`; renderFamilyTree(); } function renderFamilyTree(){ const root=family.people.find(p=>p.id===focusPerson);if(!root)return; const depth=Number($('#family-depth').value),edges=family.links.filter(e=>e.active),people=new Map(family.people.map(p=>[p.id,p]));let count=0; function branch(id,d,direction,path){ if(++count>300)return '
  • 更多分支请切换人物查看
  • '; const p=people.get(id);if(!p)return ''; const node=``; if(path.has(id))return `
  • ${node}(重复分支)
  • `; const seen=new Set([...path,id]); const next=edges.filter(e=>e.kind==='parent'&&(direction==='up'?e.toId:e.fromId)===id).map(e=>direction==='up'?e.fromId:e.toId); return `
  • ${node}${d>0&&next.length?'':next.length?' 还有 '+next.length+' 位,点击此人物继续':''}
  • `; } const spouseIds=edges.filter(e=>e.kind==='spouse'&&(e.fromId===root.id||e.toId===root.id)).map(e=>e.fromId===root.id?e.toId:e.fromId); $('#family-tree').innerHTML=`

    上溯祖先

    下续后裔

    配偶

    ${spouseIds.map(id=>``).join(' ')||'

    尚未记录

    '}
    `; } function openPerson(id){if(!family.canEdit)return;const p=family.people.find(p=>p.id===id)||{};fillForm($('#person-form'),{...p,revision:family.revision});initDateFields();$('#person-dialog').showModal();} function openRelation(id){if(!family.canEdit)return;if(family.people.length<2){toast('请先录入另一位亲属,再连接关系');return;}const f=$('#relation-form');f.elements.fromId.innerHTML=peopleOptions(false);f.elements.toId.innerHTML=peopleOptions(false);const edge=family.links.find(e=>e.id===id);fillForm(f,{...(edge||{fromId:focusPerson,active:true}),revision:family.revision});if(!edge)f.elements.toId.value=family.people.find(p=>p.id!==focusPerson)?.id||'';$('#relation-dialog').showModal();} for(const [name,path]of [['person','person'],['relation','link']]){ $('#'+name+'-form').addEventListener('submit',async e=>{ e.preventDefault();const f=e.currentTarget,b=f.querySelector('[type=submit]');b.disabled=true; try{const d=formValues(f);d.revision=Number(d.revision);if(name==='person'){syncDateFields(true);Object.assign(d,formValues(f));d.revision=Number(d.revision);d.archived=f.elements.archived.checked;}else d.active=f.elements.active.checked; const result=await api('/api/family/'+path,d);if(name==='person')focusPerson=result.item.id;f.closest('dialog').close();await loadFamily();toast('家谱已保存'); }catch(err){f.querySelector('.form-error').textContent=err.message;}finally{b.disabled=false;} }); } async function loadUsers(){ if(!isAdmin())return;const id=currentUser.id; try{const [u,f]=await Promise.all([api('/api/users'),api('/api/family')]);if(!loggedIn||currentUser?.id!==id)return;users=u.users;family=f; $('#user-list').innerHTML=`
    ${users.map(u=>``).join('')}
    账号角色 / 状态人物关系家谱权限摄像头空间
    ${esc(u.name)}${esc(u.username)}${u.role==='admin'?'管理员':'个人账号'}${u.disabled?' · 已停用':''}${u.relationship==='本人'&&u.relatedToId===u.id?'本人':u.relationship?esc(u.name+' 是 '+(users.find(x=>x.id===u.relatedToId)?.name||'(参照待指定)')+' 的 '+u.relationship):'未设置'}家谱:${esc(personName(u.personId))}${u.role==='admin'?'管理':({none:'不可访问',read:'查看',edit:'录入和维护'}[u.familyAccess])}${u.role==='admin'?'全部空间':esc(u.siteIds.map(siteName).join('、')||'未授权')}
    `; }catch(e){toast(e.message);} } function openUser(id){ if(!isAdmin())return;const f=$('#user-form'),u=users.find(u=>u.id===id)||{}; f.elements.personId.innerHTML=peopleOptions();f.elements.relatedToId.innerHTML=''+users.map(u=>``).join(''); fillForm(f,u);f.elements.password.required=!u.id; $('#user-sites').innerHTML=state.sites.map(s=>``).join('');$('#user-dialog').showModal(); } $('#user-form').addEventListener('submit',async e=>{e.preventDefault();const f=e.currentTarget,b=f.querySelector('[type=submit]');b.disabled=true;try{const d=formValues(f);d.siteIds=[...f.querySelectorAll('[name=siteIds]:checked')].map(el=>el.value);d.disabled=f.elements.disabled.checked;d.ptzControl=f.elements.ptzControl.checked;await api('/api/users',d);f.closest('dialog').close();await auth();if(loggedIn)await loadUsers();toast('账号已保存');}catch(err){f.querySelector('.form-error').textContent=err.message;}finally{b.disabled=false;}}); $('#new-person').onclick=()=>openPerson();$('#new-user').onclick=()=>openUser();$('#family-reload').onclick=loadFamily; $('#family-reference').onchange=renderFamily;$('#family-search').oninput=renderFamily;$('#family-archived').onchange=renderFamily;$('#family-depth').onchange=renderFamilyTree; $('#family-export').onclick=async()=>{try{const data=await api('/api/family');const blob=new Blob([JSON.stringify({format:'zhaofu-family',version:1,exportedAt:new Date().toISOString(),people:data.people,links:data.links},null,2)],{type:'application/json'});const link=document.createElement('a'),href=URL.createObjectURL(blob);link.href=href;link.download='zhaofu-family-'+new Date().toISOString().slice(0,10)+'.json';link.click();setTimeout(()=>URL.revokeObjectURL(href),60000);}catch(e){toast(e.message);}}; document.addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;if(b.dataset.person){focusPerson=b.dataset.person;renderFamily();}if(b.dataset.editPerson)openPerson(b.dataset.editPerson);if(b.id==='add-relation')openRelation();if(b.dataset.editRelation)openRelation(b.dataset.editRelation);if(b.dataset.editUser)openUser(b.dataset.editUser);});