Release v0.1.29: media previews, clean composer, regional profiles and cloud retention
This commit is contained in:
@@ -296,3 +296,26 @@ html[data-theme=ink] body{background-image:radial-gradient(ellipse at 90% 0,#263
|
||||
@media(max-width:760px){.brand-image-grid{grid-template-columns:1fr}.home-cover-content{padding:20px}.home-card-body{padding:20px}.home-cover-placeholder{font-size:15px}.sidebar #logout{display:block}#page-settings .form-grid{grid-template-columns:1fr}html[data-density=compact] main{padding:18px 14px}#household-form .form-grid{grid-template-columns:1fr}.home-city-group>h3 small{display:block;margin:8px 0}}
|
||||
/* Personal journals: a quiet paper timeline with bounded media panels. */
|
||||
.journal-feed{max-width:1080px;margin:0 auto}.journal-compose textarea{resize:vertical;min-height:130px}.journal-compose>label{display:grid;gap:12px}.journal-add-media,.journal-reactions{display:flex;flex-wrap:wrap;gap:12px;margin:20px 0}.journal-add-media>label{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.journal-add-media input{max-width:260px}.journal-filters{display:flex;align-items:end;flex-wrap:wrap;gap:16px}.journal-filters label{flex:1;min-width:145px;display:grid;gap:7px}.journal-entry{border-left:3px solid #a98964;margin-bottom:26px}.journal-entry .section-head small{display:block;color:var(--muted);margin-top:6px}.journal-text{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.95;font-size:17px}.journal-author{font-size:20px!important;font-family:serif}.journal-media{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:14px}.journal-media figure{margin:0;min-width:0}.journal-media img,.journal-media video{width:100%;max-height:450px;object-fit:contain;border-radius:8px;background:#1a2925}.journal-media audio{width:100%;max-width:100%}.journal-media figcaption{font-size:13px;overflow-wrap:anywhere;margin-top:6px}.journal-image-load{width:100%;min-height:150px;background:#e9eee2!important}.journal-upload{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;border-bottom:1px solid #d9d7c8;padding:12px 0}.journal-upload label{flex:1;overflow-wrap:anywhere}.journal-upload small{display:block}.journal-upload input[type=checkbox]{width:auto}.journal-resume input{max-width:220px}.journal-comments{border-top:1px solid #d9d7c8;padding-top:15px}.journal-comments form{display:flex;align-items:end;gap:12px;flex-wrap:wrap}.journal-comments form label{flex:1;min-width:170px}.journal-comment{padding:10px 0;border-bottom:1px dashed #d9d7c8;overflow-wrap:anywhere}.journal-comment small{font-size:11px;color:var(--muted)}.journal-reactions [aria-pressed=true]{color:#ad5842!important}.journal-comments .quiet{padding:4px 8px;font-size:12px}.journal-feed .panel{overflow:hidden}html[data-theme=ink] .journal-image-load{background:#293f37!important}
|
||||
.journal-upload-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,280px),1fr));gap:18px;margin:20px 0}
|
||||
.journal-upload-card{border:1px solid var(--line,#d9d7c8);border-radius:12px;padding:16px;background:var(--surface,#fffdf8);min-width:0;display:flex;flex-direction:column;gap:14px}
|
||||
.journal-upload-card:has(input:checked){border-color:#456f5f;box-shadow:0 0 0 1px #456f5f}
|
||||
.journal-upload-select{display:flex!important;align-items:flex-start;gap:10px;overflow-wrap:anywhere;line-height:1.5}
|
||||
.journal-upload-select input[type=checkbox]{width:18px;min-width:18px;height:18px;margin-top:3px;accent-color:#315a4e}
|
||||
.journal-upload-select small{display:block;font-size:12px;color:var(--muted);margin-top:4px}
|
||||
.journal-card-actions{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin-top:auto}
|
||||
.journal-card-actions button{padding:8px 13px;font-size:13px}.journal-card-actions [data-remove-upload]{margin-left:auto;color:#954f40}
|
||||
.journal-card-actions a,.journal-media-caption a{font-size:13px;color:var(--accent,#315a4e)}
|
||||
.journal-attachment{margin:0;min-width:0}.journal-photo{display:block;position:relative;width:100%;padding:0!important;border:0!important;background:#e6e9df!important;border-radius:9px;overflow:hidden;color:#fff!important}
|
||||
.journal-photo img{display:block;width:100%;min-height:120px;max-height:450px;object-fit:contain;aspect-ratio:16/10;background:#e6e9df;border-radius:9px}
|
||||
.journal-photo-hint{position:absolute;right:10px;bottom:10px;padding:4px 9px;background:#213a30c9;border-radius:5px;font-size:12px}
|
||||
.journal-upload-card .journal-photo img{height:180px;min-height:0;object-fit:cover}
|
||||
.journal-attachment video{width:100%;max-height:450px;border-radius:8px;background:#1a2925}.journal-upload-card video{height:180px}
|
||||
.journal-attachment audio{width:100%;max-width:100%}.journal-audio{background:#e9eee5;border:1px solid #d5decd;border-radius:10px;padding:14px}
|
||||
.journal-audio-label{display:flex;align-items:center;gap:10px;color:#42614f;font-size:13px;letter-spacing:1px;margin:0 0 14px}.journal-audio-label span{font-size:28px}
|
||||
.journal-media-state{display:block;font-size:12px;color:var(--muted);padding-top:6px;min-height:0}.journal-media-state:empty{display:none}
|
||||
.journal-media-caption{font-size:12px;color:var(--muted);overflow-wrap:anywhere;margin:8px 0 16px}
|
||||
.journal-trash{padding:12px 0;border-top:1px solid #d9d7c8}.journal-trash-row{display:flex;align-items:center;gap:15px;justify-content:space-between;padding:10px 0;border-bottom:1px solid #d9d7c8;overflow-wrap:anywhere}
|
||||
.journal-preview{width:min(960px,94vw);max-width:94vw;padding:24px;max-height:94vh}.journal-preview header,.journal-preview footer{display:flex;align-items:center;justify-content:space-between;gap:18px;position:static}.journal-preview header{margin-bottom:20px}.journal-preview h2{font-size:22px;overflow-wrap:anywhere;margin:0}.journal-preview footer{font-size:13px;margin-top:18px}
|
||||
.journal-preview .journal-photo img{max-height:65vh;min-height:0;aspect-ratio:auto}.journal-preview .journal-photo{pointer-events:none}.journal-preview .journal-photo-hint{display:none}.journal-preview video{max-height:65vh}
|
||||
html[data-theme=ink] .journal-upload-card{background:#20352d}html[data-theme=ink] .journal-audio{background:#293f37;border-color:#436152}html[data-theme=ink] .journal-audio-label{color:#d5e2ce}
|
||||
@media(max-width:600px){.journal-upload-grid{grid-template-columns:1fr}.journal-preview{padding:16px}.journal-preview h2{font-size:17px}.journal-preview header button{flex-shrink:0}.journal-media{grid-template-columns:1fr}}
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
'use strict';
|
||||
const historyFields={name:'姓名',alias:'别名',gender:'性别',lifeStatus:'在世情况',birthDate:'出生日期',deathDate:'去世日期',birthplace:'籍贯',biography:'生平',note:'备注',archived:'归档',fromId:'关系一方',toId:'关系另一方',kind:'关系类型',lineage:'亲子属性',active:'有效关系',title:'记事标题',eventDate:'发生日期',summary:'摘要',body:'正文',personIds:'关联人物',status:'发布状态',sourceName:'资料来源',sourceUrl:'报道链接'};
|
||||
historyFields.nativeCountry='籍贯 · 国家/地区';
|
||||
historyFields.nativeProvince='籍贯 · 省/自治区/直辖市';
|
||||
historyFields.nativeCity='籍贯 · 地级市/州/盟';
|
||||
historyFields.nativeDistrict='籍贯 · 区/县/县级市';
|
||||
historyFields.nativeTown='籍贯 · 乡镇/街道';
|
||||
historyFields.nativeCommunity='籍贯 · 村/社区';
|
||||
historyFields.nativeAddress='籍贯 · 详细地址/历史地名';
|
||||
historyFields.birthCountry='出生地 · 国家/地区';
|
||||
historyFields.birthProvince='出生地 · 省/自治区/直辖市';
|
||||
historyFields.birthCity='出生地 · 地级市/州/盟';
|
||||
historyFields.birthDistrict='出生地 · 区/县/县级市';
|
||||
historyFields.birthTown='出生地 · 乡镇/街道';
|
||||
historyFields.birthCommunity='出生地 · 村/社区';
|
||||
historyFields.birthAddress='出生地 · 详细地址/历史地名';
|
||||
|
||||
let historyContext=null,historySelection=null,historyGeneration=0,historyDetailGeneration=0;
|
||||
function clearFamilyHistory(){historyGeneration++;historyDetailGeneration++;historyContext=null;historySelection=null;for(const id of ['history-list','history-detail','history-error'])$('#'+id).replaceChildren();}
|
||||
function historyValue(key,value){
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ class FamilyMap {
|
||||
const byId=new Map(data.people.map(p=>[p.id,p]));
|
||||
this.viewport.querySelectorAll('[data-map-person]').forEach(b=>{
|
||||
const p=byId.get(b.dataset.mapPerson),self=p.id===data.selfId,selected=p.id===data.focusId;
|
||||
const matched=data.term&&[p.name,p.alias,p.birthplace].join(' ').toLowerCase().includes(data.term);
|
||||
const matched=data.term&&personSearch(p).includes(data.term);
|
||||
const relation=p.id===data.reference&&!self?'参照人物':ZhaoKinship.describe(data.people,data.links,data.reference,p.id);
|
||||
b.classList.toggle('self',self);b.classList.toggle('selected',selected);b.classList.toggle('search-hit',!!matched);b.classList.toggle('archived',!!p.archived);
|
||||
b.setAttribute('aria-pressed',String(selected));b.setAttribute('aria-label',p.name+(self?',本人':'')+','+relation+',查看人物档案');
|
||||
|
||||
+6
-4
@@ -6,6 +6,8 @@ const familyMap=new FamilyMap($('#family-tree'),id=>selectFamilyPerson(id));
|
||||
const relationships=['本人','配偶','哥哥','弟弟','姐姐','妹妹','爸爸','妈妈','爷爷','奶奶','姥爷','姥姥','伯伯','叔叔','姑姑','舅舅','姨妈','儿子','女儿','孙子','孙女','外孙','外孙女','曾孙','曾孙女','玄孙','玄孙女','祖先','后裔','亲友','同事'];
|
||||
$('#relationship-choices').innerHTML=relationships.map(r=>`<option value="${esc(r)}"></option>`).join('');
|
||||
function clearFamilyState(){if(typeof clearFamilyHistory==='function')clearFamilyHistory();closeFamilyFullscreen();familyGeneration++;family={people:[],links:[],events:[],revision:0,canEdit:false};users=[];focusPerson='';familyMap.reset();if(typeof resetFamilyEvents==='function')resetFamilyEvents(true);$('#family-map-note').textContent='';$('#family-count').textContent='';$('#family-reference').replaceChildren();delete $('#family-reference').dataset.initialized;$('#family-search').value='';$('#family-archived').checked=false;$('#family-find').hidden=true;}
|
||||
function personLocation(p,prefix){return ['Country','Province','City','District','Town','Community','Address'].map(k=>p[prefix+k]).filter(Boolean).join(' · ');}
|
||||
function personSearch(p){return [p.name,p.alias,p.birthplace,personLocation(p,'native'),personLocation(p,'birth')].join(' ').toLowerCase();}
|
||||
function personName(id){return family.people.find(p=>p.id===id)?.name||'未关联';}
|
||||
function peopleOptions(blank=true){return (blank?'<option value="">尚未关联</option>':'')+family.people.map(p=>`<option value="${p.id}">${esc(p.name+(p.archived?'(已归档)':''))}</option>`).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??'';}}
|
||||
@@ -30,7 +32,7 @@ async function loadFamily(resetView=false){
|
||||
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));
|
||||
const people=family.people.filter(p=>($('#family-archived').checked||!p.archived)&&personSearch(p).includes(term));
|
||||
$('#family-count').textContent=family.people.filter(p=>!p.archived).length+' 位人物 · '+family.links.filter(e=>e.active).length+' 条关系'+(term?' · 找到 '+people.length+' 位':'');
|
||||
$('#family-find').hidden=!term;$('#family-find').disabled=!people.length;
|
||||
$('#new-person').hidden=!family.canEdit;
|
||||
@@ -40,7 +42,7 @@ function renderFamily(){
|
||||
$('#family-detail').classList.toggle('is-self',p?.id===family.selfId&&!!p);
|
||||
if(!p){$('#family-detail').innerHTML='<h3>人物档案</h3><p class="description">点击图中的人物查看生平,或录入第一位人物。</p>';return;}
|
||||
const edges=family.links.filter(e=>e.fromId===p.id||e.toId===p.id);
|
||||
$('#family-detail').innerHTML=`<div class="section-head"><div><span class="eyebrow">人物档案</span><h2>${esc(p.name)}</h2></div>${family.canEdit?`<button data-edit-person="${p.id}" class="quiet">编辑人物</button><button class="quiet" data-history-kind="person" data-history-id="${p.id}">修改记录</button>`:''}</div><p class="description">相对于 ${esc(personName(reference))}:${esc(p.id===reference&&p.id!==family.selfId?'参照人物':ZhaoKinship.describe(family.people,family.links,reference,p.id))}</p><dl>${[['别名',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])=>`<div><dt>${k}</dt><dd>${esc(v||'待补充')}</dd></div>`).join('')}</dl><h3>生平与记载</h3><p class="family-prose">${esc(p.biography||'尚未记录生平')}</p><p class="description family-prose">${esc(p.note||'')}</p><div class="section-head"><h3>亲属关系</h3>${family.canEdit?'<button id="add-relation" class="quiet">+ 连接亲属</button>':''}</div>${edges.map(e=>`<div class="family-link"><span>${esc(relationText(e))}<small>${esc(({biological:'亲生',adoptive:'收养',step:'继亲',unspecified:''}[e.lineage]||'')+' '+(e.note||''))}</small></span>${family.canEdit?`<button class="quiet" data-edit-relation="${e.id}">编辑</button><button class="quiet" data-history-kind="link" data-history-id="${e.id}">历史</button>`:''}</div>`).join('')||'<p class="description">尚未连接亲属。</p>'}`;
|
||||
$('#family-detail').innerHTML=`<div class="section-head"><div><span class="eyebrow">人物档案</span><h2>${esc(p.name)}</h2></div>${family.canEdit?`<button data-edit-person="${p.id}" class="quiet">编辑人物</button><button class="quiet" data-history-kind="person" data-history-id="${p.id}">修改记录</button>`:''}</div><p class="description">相对于 ${esc(personName(reference))}:${esc(p.id===reference&&p.id!==family.selfId?'参照人物':ZhaoKinship.describe(family.people,family.links,reference,p.id))}</p><dl>${[['别名',p.alias],['性别',{male:'男',female:'女',unknown:'待确认'}[p.gender]],['在世情况',{alive:'在世',deceased:'已故',unknown:'待确认'}[p.lifeStatus]],['出生日期',ZhaoCalendar.describe(p.birthDate)],...(p.lifeStatus==='deceased'?[['去世日期',ZhaoCalendar.describe(p.deathDate)]]:[]),['籍贯(祖籍)',personLocation(p,'native')],['出生地',personLocation(p,'birth')],...(p.birthplace?[['原地域记录',p.birthplace]]:[])].map(([k,v])=>`<div><dt>${k}</dt><dd>${esc(v||'待补充')}</dd></div>`).join('')}</dl><h3>生平与记载</h3><p class="family-prose">${esc(p.biography||'尚未记录生平')}</p><p class="description family-prose">${esc(p.note||'')}</p><div class="section-head"><h3>亲属关系</h3>${family.canEdit?'<button id="add-relation" class="quiet">+ 连接亲属</button>':''}</div>${edges.map(e=>`<div class="family-link"><span>${esc(relationText(e))}<small>${esc(({biological:'亲生',adoptive:'收养',step:'继亲',unspecified:''}[e.lineage]||'')+' '+(e.note||''))}</small></span>${family.canEdit?`<button class="quiet" data-edit-relation="${e.id}">编辑</button><button class="quiet" data-history-kind="link" data-history-id="${e.id}">历史</button>`:''}</div>`).join('')||'<p class="description">尚未连接亲属。</p>'}`;
|
||||
if(typeof personEventSummary==='function')$('#family-detail').insertAdjacentHTML('beforeend',personEventSummary(p.id));
|
||||
$('#family-detail').insertAdjacentHTML('afterbegin',`${p.id===family.selfId?'<div class="family-self-heading">◎ 本人 · 当前账号关联人物</div>':''}<button type="button" id="family-show-person" class="quiet family-back-to-map">在图中定位</button>`);
|
||||
}
|
||||
@@ -54,7 +56,7 @@ function renderFamilyTree(){
|
||||
familyMap.render({...family,focusId:focusPerson,reference:$('#family-reference').value,term:$('#family-search').value.trim().toLowerCase(),showArchived:$('#family-archived').checked});
|
||||
}
|
||||
function selectFamilyPerson(id,locate=false){focusPerson=id;if(locate){familyView='tree';if(family.people.find(p=>p.id===id)?.archived)$('#family-archived').checked=true;}renderFamily();if(locate)familyMap.locate(id);if(window.innerWidth<=1000&&!document.fullscreenElement)$('#family-detail').scrollIntoView({behavior:'smooth',block:'start'});}
|
||||
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 openPerson(id){if(!family.canEdit)return;const p=family.people.find(p=>p.id===id)||{};fillForm($('#person-form'),{...p,revision:family.revision});$('#person-legacy-place').hidden=!p.birthplace;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=>{
|
||||
@@ -83,7 +85,7 @@ $('#family-reference').onchange=renderFamily;$('#family-search').oninput=renderF
|
||||
$('#family-view-tree').onclick=()=>{familyView='tree';renderFamily();familyMap.fit();};$('#family-view-list').onclick=()=>{familyView='list';renderFamily();};
|
||||
$('#family-zoom-in').onclick=()=>familyMap.zoom(1.25);$('#family-zoom-out').onclick=()=>familyMap.zoom(.8);$('#family-fit').onclick=()=>familyMap.fit();
|
||||
$('#family-locate-self').onclick=()=>selectFamilyPerson(family.selfId,true);
|
||||
$('#family-find').onclick=()=>{const term=$('#family-search').value.trim().toLowerCase(),matches=family.people.filter(p=>($('#family-archived').checked||!p.archived)&&[p.name,p.alias,p.birthplace].join(' ').toLowerCase().includes(term));if(!matches.length)return;const index=matches.findIndex(p=>p.id===focusPerson);selectFamilyPerson(matches[(index+1)%matches.length].id,true);};
|
||||
$('#family-find').onclick=()=>{const term=$('#family-search').value.trim().toLowerCase(),matches=family.people.filter(p=>($('#family-archived').checked||!p.archived)&&personSearch(p).includes(term));if(!matches.length)return;const index=matches.findIndex(p=>p.id===focusPerson);selectFamilyPerson(matches[(index+1)%matches.length].id,true);};
|
||||
let familyBodyOverflow='';
|
||||
function syncFamilyFullscreen(){
|
||||
const expanded=document.fullscreenElement===$('#family-workspace')||$('#family-workspace').classList.contains('family-expanded');
|
||||
|
||||
+4
-3
File diff suppressed because one or more lines are too long
+78
-21
@@ -1,13 +1,47 @@
|
||||
'use strict';
|
||||
pageInfo.journal=['个人记事','一言一照,都是阅历。私藏或分享,由自己决定。'];
|
||||
let journalGeneration=0,journalData=null,journalPage=0,journalUpload=false,journalRequest=null,journalRecorder=null,journalTracks=null,journalVoice=[],journalVoiceTimer=null,journalConfigRevision=0;
|
||||
const journalImages=new Map();let journalImageBytes=0;
|
||||
const journalDraftMedia=new Set(),journalSelectedMedia=new Set();
|
||||
const journalId=()=>[...crypto.getRandomValues(new Uint8Array(16))].map(b=>b.toString(16).padStart(2,'0')).join('');
|
||||
const journalToday=()=>{const d=new Date();return [d.getFullYear(),String(d.getMonth()+1).padStart(2,'0'),String(d.getDate()).padStart(2,'0')].join('-');};
|
||||
function journalBusy(value){journalUpload=value;$('#journal-save').disabled=value;$('#journal-new').disabled=value;for(const control of $('#journal-filter').elements)control.disabled=value;$('#journal-prev').disabled=value||!journalData?.page;$('#journal-next').disabled=value||!journalData||(journalData.page+1)*20>=journalData.total;$('#journal-record').disabled=value||!journalData?.connection.connected||!window.isSecureContext||!navigator.mediaDevices?.getUserMedia||!window.MediaRecorder;}
|
||||
function journalMessage(text){$('#journal-status').textContent=text;}
|
||||
function journalValid(g){return g===journalGeneration&&loggedIn&&activePage==='journal';}
|
||||
function journalMediaUrl(id,download=false){return '/media/journal?'+new URLSearchParams({id,family:state.familyId,...(download?{download:'1'}:{})});}
|
||||
function releaseJournalMedia(root){
|
||||
root.querySelectorAll('audio,video').forEach(v=>{v.pause();v.removeAttribute('src');v.load();});
|
||||
root.querySelectorAll('img').forEach(img=>img.removeAttribute('src'));
|
||||
}
|
||||
function journalMediaView(m){
|
||||
const name=esc(m.name),id=esc(m.id),src=esc(journalMediaUrl(m.id));
|
||||
if(m.mime.startsWith('image/'))return `<figure class="journal-attachment"><button type="button" class="journal-photo" data-preview-media="${id}" aria-label="放大预览 ${name}"><img src="${src}" alt="${name}" loading="lazy" decoding="async"><span class="journal-photo-hint">点击放大</span></button><span class="journal-media-state" role="status">照片加载中…</span><button type="button" class="quiet" data-retry-media hidden>重新加载照片</button></figure>`;
|
||||
const tag=m.mime.startsWith('audio/')?'audio':'video';
|
||||
return `<figure class="journal-attachment journal-${tag}">${tag==='audio'?'<div class="journal-audio-label"><span aria-hidden="true">♪</span> 语音与音频</div>':''}<${tag} controls preload="metadata" playsinline src="${src}" aria-label="${name}"></${tag}><span class="journal-media-state" role="status">正在加载${tag==='audio'?'音频':'视频'}…</span><button type="button" class="quiet" data-retry-media hidden>重新加载</button></figure>`;
|
||||
}
|
||||
function bindJournalMedia(root){
|
||||
root.querySelectorAll('.journal-attachment').forEach(box=>{
|
||||
const media=box.querySelector('img,audio,video'),message=box.querySelector('.journal-media-state'),retry=box.querySelector('[data-retry-media]');
|
||||
const ready=()=>{message.textContent='';retry.hidden=true;box.classList.remove('media-error');};
|
||||
media.addEventListener(media.tagName==='IMG'?'load':'loadedmetadata',ready);
|
||||
media.addEventListener('error',()=>{message.textContent=media.tagName==='IMG'?'照片暂未加载,请重试。':'暂时无法播放,请重试或下载原文件。';retry.hidden=false;box.classList.add('media-error');});
|
||||
if(media.tagName==='IMG'&&media.complete&&media.naturalWidth)ready();
|
||||
if(media.tagName!=='IMG'){
|
||||
if(media.readyState>=1)ready();
|
||||
media.addEventListener('waiting',()=>{message.textContent='正在缓冲…';});
|
||||
media.addEventListener('playing',ready);media.addEventListener('canplay',ready);
|
||||
media.addEventListener('play',()=>{$$('#journal-list audio,#journal-list video,#journal-upload-list audio,#journal-upload-list video,#journal-library-list audio,#journal-library-list video,#journal-preview audio,#journal-preview video').forEach(other=>{if(other!==media)other.pause();});});
|
||||
}
|
||||
});
|
||||
}
|
||||
function previewJournalMedia(id){
|
||||
const all=[...(journalData?.uploads||[]),...(journalData?.trash||[]),...(journalData?.items||[]).flatMap(e=>e.media)];
|
||||
const media=all.find(m=>m.id===id&&m.status==='ready');if(!media)return;
|
||||
const dialog=$('#journal-preview');releaseJournalMedia($('#journal-preview-content'));
|
||||
$('#journal-preview-title').textContent=media.name;$('#journal-preview-download').href=journalMediaUrl(id,true);
|
||||
$('#journal-preview-content').innerHTML=journalMediaView(media);bindJournalMedia($('#journal-preview-content'));if(!dialog.open)dialog.showModal();
|
||||
}
|
||||
$('#journal-preview-close').onclick=()=>$('#journal-preview').close();
|
||||
$('#journal-preview').addEventListener('close',()=>{releaseJournalMedia($('#journal-preview-content'));$('#journal-preview-content').replaceChildren();$('#journal-preview-download').removeAttribute('href');});
|
||||
function stopJournalVoice(discard=true){
|
||||
if(discard)journalVoice=[];if(journalRecorder&&journalRecorder.state!=='inactive')journalRecorder.stop();
|
||||
journalTracks?.getTracks().forEach(t=>t.stop());journalTracks=null;clearTimeout(journalVoiceTimer);journalVoiceTimer=null;
|
||||
@@ -15,13 +49,13 @@ function stopJournalVoice(discard=true){
|
||||
}
|
||||
function leaveJournal(){
|
||||
journalGeneration++;journalRequest?.abort();journalRequest=null;journalBusy(false);stopJournalVoice();
|
||||
$$('#journal-list video,#journal-list audio').forEach(v=>{v.pause();v.removeAttribute('src');v.load();});
|
||||
for(const image of journalImages.values())URL.revokeObjectURL(image.url);journalImages.clear();journalImageBytes=0;
|
||||
for(const id of ['journal-list','journal-upload-list','journal-library-list','journal-preview-content'])releaseJournalMedia($('#'+id));
|
||||
$('#journal-preview').close();
|
||||
$('#journal-list').replaceChildren();$('#journal-files').disabled=false;$('#journal-save').disabled=false;
|
||||
}
|
||||
function clearJournalForm(){const f=$('#journal-form');f.reset();for(const key of ['id','revision','clientId'])f.elements[key].value='';}
|
||||
function resetJournal(){leaveJournal();journalData=null;journalPage=0;clearJournalForm();$('#journal-filter').reset();$('#journal-upload-list').replaceChildren();$('#journal-status').textContent='';$('#journal-person').replaceChildren();$('#journal-files').disabled=true;$('#journal-record').disabled=true;$('#baidu-config').reset();$('#baidu-status').textContent='';$('#baidu-config-status').textContent='';}
|
||||
function newJournal(){clearJournalForm();const f=$('#journal-form');for(const input of $('#journal-upload-list').querySelectorAll('input[type=checkbox]'))input.checked=false;f.elements.clientId.value=journalId();f.elements.occurredOn.value=journalToday();$('#journal-save').textContent='保存记事';journalMessage('默认仅自己可见。选择“当前家庭/场所”后才会进入动态。');renderJournalUploads();}
|
||||
function resetJournal(){leaveJournal();journalDraftMedia.clear();journalSelectedMedia.clear();$('#journal-library-list').replaceChildren();$('#journal-trash-list').replaceChildren();journalData=null;journalPage=0;clearJournalForm();$('#journal-filter').reset();$('#journal-upload-list').replaceChildren();$('#journal-status').textContent='';$('#journal-person').replaceChildren();$('#journal-files').disabled=true;$('#journal-record').disabled=true;$('#baidu-config').reset();$('#baidu-status').textContent='';$('#baidu-config-status').textContent='';}
|
||||
function newJournal(){clearJournalForm();stopJournalVoice();journalDraftMedia.clear();journalSelectedMedia.clear();$('#journal-library').open=false;const f=$('#journal-form');for(const input of $('#journal-upload-list').querySelectorAll('input[type=checkbox]'))input.checked=false;f.elements.clientId.value=journalId();f.elements.occurredOn.value=journalToday();$('#journal-save').textContent='保存记事';journalMessage('默认仅自己可见。选择“当前家庭/场所”后才会进入动态。');renderJournalUploads();}
|
||||
async function loadJournal(){
|
||||
const g=++journalGeneration;if(!loggedIn)return;
|
||||
if(!$('#journal-form').elements.clientId.value)newJournal();
|
||||
@@ -39,15 +73,29 @@ async function loadJournal(){
|
||||
renderJournalUploads();renderJournalEntries();
|
||||
}catch(e){if(!e.stale&&journalValid(g))$('#journal-query-status').textContent=e.message;}
|
||||
}
|
||||
function journalUploadCard(m,library=false){
|
||||
const id=esc(m.id),ready=m.status==='ready';
|
||||
return `<div class="journal-upload-card" data-upload-card="${id}"><${library?'div':'label'} class="journal-upload-select">${library?'':`<input type="checkbox" value="${id}" ${ready?'':'disabled'} ${journalSelectedMedia.has(m.id)?'checked':''}>`}<span>${esc(m.name)}<small>${Math.ceil(m.size/1024)} KB · ${ready?'已存百度网盘':'等待继续上传'}</small></span></${library?'div':'label'}>${ready?journalMediaView(m):`<p class="description">${esc(m.error||'上传尚未完成,请选择原文件继续。')}</p>`}<div class="journal-card-actions">${ready?`<button type="button" class="quiet" data-preview-media="${id}">预览</button>${library?`<button type="button" class="quiet" data-use-upload="${id}">用于本条</button>`:`<button type="button" class="quiet" data-detach-upload="${id}">从本条移除</button>`}`:`<label class="quiet journal-resume">继续上传<input type="file" data-journal-resume="${id}"></label>`}<button type="button" class="quiet" data-remove-upload="${id}">删除</button></div></div>`;
|
||||
}
|
||||
function renderJournalUploads(){
|
||||
const selected=new Set([...$('#journal-upload-list').querySelectorAll('input:checked')].map(e=>e.value));
|
||||
const editing=journalData?.items.find(e=>e.id===$('#journal-form').elements.id.value);
|
||||
const available=[...(journalData?.uploads||[])];for(const media of editing?.media||[])if(!available.some(m=>m.id===media.id))available.push(media);
|
||||
$('#journal-upload-list').innerHTML=available.map(m=>`<div class="journal-upload"><label><input type="checkbox" value="${esc(m.id)}" ${m.status==='ready'?'':'disabled'} ${selected.has(m.id)?'checked':''}>${esc(m.name)} <small>${Math.ceil(m.size/1024)} KB · ${m.status==='ready'?'已存百度网盘':m.status==='receiving'?'等待继续上传':'已失效'}</small></label>${m.status==='receiving'?`<label class="quiet journal-resume">选择原文件继续<input type="file" data-journal-resume="${esc(m.id)}"></label>`:''}<button type="button" class="quiet" data-remove-upload="${esc(m.id)}">移出列表</button></div>`).join('');
|
||||
releaseJournalMedia($('#journal-upload-list'));releaseJournalMedia($('#journal-library-list'));
|
||||
$('#journal-upload-list').innerHTML=available.filter(m=>journalDraftMedia.has(m.id)).map(m=>journalUploadCard(m)).join('');
|
||||
const library=(journalData?.uploads||[]).filter(m=>!journalDraftMedia.has(m.id));
|
||||
$('#journal-library-count').textContent=String(library.length);
|
||||
$('#journal-library-list').innerHTML=$('#journal-library').open?(library.map(m=>journalUploadCard(m,true)).join('')||'<p class="description">暂无其他附件。</p>'):'';
|
||||
const trash=journalData?.trash||[];$('#journal-trash').hidden=!trash.length;
|
||||
$('#journal-trash-count').textContent=String(trash.length);
|
||||
$('#journal-trash-list').innerHTML=trash.map(m=>`<div class="journal-trash-row"><span>${esc(m.name)}</span><button type="button" class="quiet" data-restore-upload="${esc(m.id)}">恢复</button></div>`).join('');
|
||||
bindJournalMedia($('#journal-upload-list'));bindJournalMedia($('#journal-library-list'));
|
||||
}
|
||||
$('#journal-library').addEventListener('toggle',renderJournalUploads);
|
||||
$('#journal-upload-list').addEventListener('change',e=>{if(e.target.type==='checkbox'){if(e.target.checked)journalSelectedMedia.add(e.target.value);else journalSelectedMedia.delete(e.target.value);}});
|
||||
function renderJournalEntries(){
|
||||
for(const value of journalImages.values())URL.revokeObjectURL(value.url);journalImages.clear();journalImageBytes=0;
|
||||
$('#journal-list').innerHTML=journalData.items.map(e=>`<article class="panel journal-entry" data-entry="${esc(e.id)}"><div class="section-head"><div><button class="quiet journal-author" data-journal-person="${esc(e.ownerId)}">${esc(e.authorName)}</button><small>${esc(e.occurredOn)} · ${esc(e.category)} · ${e.visibility==='private'?'仅自己可见':'当前家庭/场所'}</small></div>${e.canEdit?`<div><button class="quiet" data-journal-edit="${esc(e.id)}">编辑</button><button class="quiet" data-journal-delete="${esc(e.id)}">删除</button></div>`:''}</div><p class="journal-text">${esc(e.text)}</p><div class="journal-media">${e.media.map(m=>m.mime.startsWith('image/')?`<figure><button class="journal-image-load quiet" data-load-photo="${esc(m.id)}" data-size="${m.size}">查看照片 · ${esc(m.name)}</button><img hidden alt="${esc(m.name)}" loading="lazy"></figure>`:`<figure><${m.mime.startsWith('audio/')?'audio':'video'} controls preload="none" playsinline src="${esc(journalMediaUrl(m.id))}"></${m.mime.startsWith('audio/')?'audio':'video'}><figcaption>${esc(m.name)} <a href="${esc(journalMediaUrl(m.id,true))}">下载原文件</a></figcaption><small>点击播放后缓冲;编码不兼容时可下载原文件。</small></figure>`).join('')}</div><div class="journal-reactions"><button class="quiet" data-journal-like="${esc(e.id)}" aria-pressed="${e.liked}">${e.liked?'♥ 已赞':'♡ 点赞'} · ${e.likes}</button><button class="quiet" data-journal-person="${esc(e.ownerId)}">查看个人阅历</button></div><details class="journal-comments"><summary>评论 <span data-comment-count>${e.commentCount}</span></summary><div data-comments>${e.comments.map(c=>journalComment(e,c)).join('')}</div><form data-journal-comment="${esc(e.id)}"><label>写评论<input name="text" maxlength="500" required autocomplete="off"></label><button type="submit">发送</button><span class="form-error" role="status"></span></form></details></article>`).join('');
|
||||
releaseJournalMedia($('#journal-list'));
|
||||
$('#journal-list').innerHTML=journalData.items.map(e=>`<article class="panel journal-entry" data-entry="${esc(e.id)}"><div class="section-head"><div><button class="quiet journal-author" data-journal-person="${esc(e.ownerId)}">${esc(e.authorName)}</button><small>${esc(e.occurredOn)} · ${esc(e.category)} · ${e.visibility==='private'?'仅自己可见':'当前家庭/场所'}</small></div>${e.canEdit?`<div><button class="quiet" data-journal-edit="${esc(e.id)}">编辑</button><button class="quiet" data-journal-delete="${esc(e.id)}">删除</button></div>`:''}</div><p class="journal-text">${esc(e.text)}</p><div class="journal-media">${e.media.map(m=>`<div>${journalMediaView(m)}<p class="journal-media-caption">${esc(m.name)} <a href="${esc(journalMediaUrl(m.id,true))}">下载原文件</a></p></div>`).join('')}</div><div class="journal-reactions"><button class="quiet" data-journal-like="${esc(e.id)}" aria-pressed="${e.liked}">${e.liked?'♥ 已赞':'♡ 点赞'} · ${e.likes}</button><button class="quiet" data-journal-person="${esc(e.ownerId)}">查看个人阅历</button></div><details class="journal-comments"><summary>评论 <span data-comment-count>${e.commentCount}</span></summary><div data-comments>${e.comments.map(c=>journalComment(e,c)).join('')}</div><form data-journal-comment="${esc(e.id)}"><label>写评论<input name="text" maxlength="500" required autocomplete="off"></label><button type="submit">发送</button><span class="form-error" role="status"></span></form></details></article>`).join('');
|
||||
bindJournalMedia($('#journal-list'));
|
||||
}
|
||||
function journalComment(entry,c){return `<p class="journal-comment"><strong>${esc(c.authorName)}</strong> ${esc(c.text)} <small>${esc(new Date(c.createdAt).toLocaleString())}</small>${c.canDelete?` <button class="quiet" data-delete-comment="${esc(c.id)}" data-journal-entry="${esc(entry.id)}">删除评论</button>`:''}</p>`;}
|
||||
$('#journal-form').addEventListener('submit',async event=>{
|
||||
@@ -77,7 +125,7 @@ async function uploadJournalFile(file,resume=''){
|
||||
if(result.received<=offset)throw Error('上传进度未前进,请选择原文件继续');offset=result.received;journalMessage('正在上传百度网盘:'+Math.round(offset/file.size*95)+'%');
|
||||
}
|
||||
await api('/api/journal/finish',{id:task.id});if(!journalValid(g)||!journalUpload)return;
|
||||
journalUpload=false;await loadJournal();const input=$('#journal-upload-list').querySelector(`input[value="${task.id}"]`);if(input)input.checked=true;journalMessage('附件已保存到百度网盘。填写内容和可见范围后保存记事。');
|
||||
journalUpload=false;journalDraftMedia.add(task.id);journalSelectedMedia.add(task.id);await loadJournal();journalMessage('附件已保存到百度网盘。填写内容和可见范围后保存记事。');
|
||||
}catch(e){if(!e.stale&&journalValid(g)){journalMessage(e.name==='AbortError'?'已暂停上传,可选择原文件继续。':e.message);journalUpload=false;const message=$('#journal-status').textContent;await loadJournal();journalMessage(message);}}
|
||||
finally{journalBusy(false);journalRequest=null;if(activePage==='journal'){$('#journal-save').disabled=false;$('#journal-files').disabled=!journalData?.connection.connected;$('#journal-cancel-upload').hidden=true;}}
|
||||
}
|
||||
@@ -100,20 +148,29 @@ document.addEventListener('change',e=>{if(e.target.dataset.journalResume){upload
|
||||
document.addEventListener('click',async event=>{
|
||||
const b=event.target.closest('button');if(!b)return;const g=journalGeneration;
|
||||
if(b.dataset.journalPerson){$('#journal-mode').value='person';$('#journal-person-label').hidden=false;$('#journal-person').value=b.dataset.journalPerson;journalPage=0;loadJournal();return;}
|
||||
if(b.dataset.journalEdit){const e=journalData.items.find(e=>e.id===b.dataset.journalEdit);if(!e)return;const f=$('#journal-form');clearJournalForm();for(const key of ['id','revision','clientId','text','occurredOn','category','visibility'])f.elements[key].value=e[key];renderJournalUploads();$('#journal-save').textContent='保存修改';for(const input of $('#journal-upload-list').querySelectorAll('input[type=checkbox]'))input.checked=e.mediaIds.includes(input.value);f.scrollIntoView({behavior:'smooth'});return;}
|
||||
if(b.dataset.loadPhoto){
|
||||
b.disabled=true;try{
|
||||
const id=b.dataset.loadPhoto;let cached=journalImages.get(id);
|
||||
if(!cached){if(Number(b.dataset.size)>20*1024*1024||journalImageBytes+Number(b.dataset.size)>20*1024*1024)throw Error('照片较大,请下载原图查看。');
|
||||
journalImageBytes+=Number(b.dataset.size);b.textContent='照片缓冲中…';const response=await fetch(journalMediaUrl(id));if(!response.ok)throw Error('照片读取失败,请稍后重试');const blob=await response.blob();if(!journalValid(g))return;
|
||||
cached={url:URL.createObjectURL(blob),size:blob.size};journalImages.set(id,cached);}
|
||||
const img=b.parentElement.querySelector('img');img.src=cached.url;img.hidden=false;b.hidden=true;
|
||||
}catch(e){if(journalValid(g)){if(b.textContent==='照片缓冲中…')journalImageBytes=Math.max(0,journalImageBytes-Number(b.dataset.size));b.textContent=e.message;b.disabled=false;const link=document.createElement('a');link.href=journalMediaUrl(b.dataset.loadPhoto,true);link.textContent='下载照片';b.after(link);}}return;
|
||||
if(b.dataset.journalEdit){const e=journalData.items.find(e=>e.id===b.dataset.journalEdit);if(!e)return;const f=$('#journal-form');clearJournalForm();for(const key of ['id','revision','clientId','text','occurredOn','category','visibility'])f.elements[key].value=e[key];journalDraftMedia.clear();journalSelectedMedia.clear();e.mediaIds.forEach(id=>{journalDraftMedia.add(id);journalSelectedMedia.add(id);});renderJournalUploads();$('#journal-save').textContent='保存修改';for(const input of $('#journal-upload-list').querySelectorAll('input[type=checkbox]'))input.checked=e.mediaIds.includes(input.value);f.scrollIntoView({behavior:'smooth'});return;}
|
||||
if(b.dataset.useUpload){journalDraftMedia.add(b.dataset.useUpload);journalSelectedMedia.add(b.dataset.useUpload);renderJournalUploads();return;}
|
||||
if(b.dataset.detachUpload){journalDraftMedia.delete(b.dataset.detachUpload);journalSelectedMedia.delete(b.dataset.detachUpload);renderJournalUploads();return;}
|
||||
if(b.dataset.previewMedia){previewJournalMedia(b.dataset.previewMedia);return;}
|
||||
if(b.hasAttribute('data-retry-media')){
|
||||
const box=b.closest('.journal-attachment'),media=box.querySelector('img,audio,video');
|
||||
b.hidden=true;box.classList.remove('media-error');box.querySelector('.journal-media-state').textContent='正在重新加载…';
|
||||
if(media.tagName==='IMG'){const src=media.src;media.removeAttribute('src');media.src=src;}else media.load();return;
|
||||
}
|
||||
const id=b.dataset.journalLike||b.dataset.journalDelete||b.dataset.journalEntry;
|
||||
if(!id&&!b.dataset.removeUpload)return;b.disabled=true;
|
||||
if(!id&&!b.dataset.removeUpload&&!b.dataset.restoreUpload)return;b.disabled=true;
|
||||
try{
|
||||
if(b.dataset.removeUpload){await api('/api/journal/remove-upload',{id:b.dataset.removeUpload});if(journalValid(g))await loadJournal();return;}
|
||||
if(b.dataset.removeUpload||b.dataset.restoreUpload){
|
||||
if(journalUpload){journalMessage('请等待当前上传完成,或先暂停上传。');return;}
|
||||
const restore=!!b.dataset.restoreUpload;
|
||||
await api(restore?'/api/journal/restore-upload':'/api/journal/remove-upload',{id:b.dataset.restoreUpload||b.dataset.removeUpload});
|
||||
if(!journalValid(g))return;
|
||||
const result=await api('/api/journal?mode=mine');if(!journalValid(g))return;
|
||||
journalData.uploads=result.uploads;journalData.trash=result.trash;if(!restore){journalDraftMedia.delete(b.dataset.removeUpload);journalSelectedMedia.delete(b.dataset.removeUpload);}
|
||||
// Keep an editor's existing references current without replacing its draft or feed players.
|
||||
for(const entry of journalData.items)for(const media of entry.media){const current=[...result.uploads,...result.trash].find(m=>m.id===media.id);if(current)Object.assign(media,current);}
|
||||
renderJournalUploads();journalMessage(restore?'附件已恢复,可重新勾选使用。':'已移到“已删除附件”,可以恢复。已发布的记事和网盘原文件保留。');return;
|
||||
}
|
||||
const entry=journalData.items.find(e=>e.id===id);if(!entry)return;
|
||||
if(b.dataset.journalLike){await api('/api/journal/action',{id,action:'like',liked:!entry.liked});if(!journalValid(g))return;entry.liked=!entry.liked;entry.likes+=entry.liked?1:-1;b.textContent=(entry.liked?'♥ 已赞':'♡ 点赞')+' · '+entry.likes;b.setAttribute('aria-pressed',String(entry.liked));}
|
||||
if(b.dataset.journalDelete){if(!confirm('删除这条记事?网盘原文件保留。'))return;await api('/api/journal/action',{id,action:'delete',revision:entry.revision});if(journalValid(g))loadJournal();}
|
||||
|
||||
@@ -109,11 +109,11 @@ $('#brand-form').addEventListener('submit',async e=>{
|
||||
$('#brand-export').onclick=()=>{if(!brandDraft)return;const {revision,...value}=brandPayload(),blob=new Blob([JSON.stringify({format:'digital-steward-appearance',version:1,config:value},null,2)],{type:'application/json'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='digital-steward-appearance.json';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};
|
||||
$('#brand-import').onchange=async e=>{const generation=appearanceGeneration;invalidateBrandImages();const job=nextImageJob('import');imageJobs.get('import').pending=true;try{const file=e.target.files[0];if(!file||file.size>1024*1024)throw Error('配置文件须小于 1 MB');const value=JSON.parse(await file.text());if(generation!==appearanceGeneration||!imageJobCurrent('import',job))return;if(value.format!=='digital-steward-appearance'||value.version!==1||!value.config||!brandDraft)throw Error('发布配置格式不正确');const config=value.config;if(Object.keys(config).sort().join('|')!==['name','subtitle','motto','loginTitle','loginNote','footer','defaults','logoData','iconData'].sort().join('|'))throw Error('配置字段不匹配');for(const k of Object.keys(appearanceChoices))if(!appearanceChoices[k][config.defaults?.[k]])throw Error('默认外观选项不正确');for(const k of ['logoData','iconData'])if(config[k]&&!/^data:image\/png;base64,[A-Za-z0-9+/=]+$/.test(config[k]))throw Error('图片格式不正确');fillBrandForm({...config,revision:brandDraft.revision});$('#brand-status').textContent='已读入表单,请核对名称、图片与默认外观后保存。';}catch(e){if(generation===appearanceGeneration&&imageJobCurrent('import',job))$('#brand-status').textContent=e.message;}finally{if(imageJobCurrent('import',job))imageJobs.get('import').pending=false;}};
|
||||
function fillBackup(value){const c=value.config,f=$('#backup-form');backupRevision=c.revision;f.elements.enabled.checked=c.enabled;if(f.elements.baidu)f.elements.baidu.checked=!!c.baidu;for(const k of ['time','keep','networkPath'])f.elements[k].value=c[k];renderBackup(value);}
|
||||
function backupLabel(r){return !r?'尚未备份':r.error?(r.localOk?'本机成功;后续备份步骤未完成':'备份失败'):r.baiduOk?'本机备份完成,百度网盘已确认文件':r.networkOk?'本机和网络目录副本均已校验':'本机备份已校验';}
|
||||
function backupLabel(r){return !r?'尚未备份':r.error?(r.localOk?'本机成功;后续备份步骤未完成':'备份失败'):r.baiduOk?(r.cloudCleanupError?'网盘备份已校验;过期副本清理待重试':r.cloudCleanupOk?'网盘备份已校验,过期副本清理完成':'本机备份完成,百度网盘已确认文件'):r.networkOk?'本机和网络目录副本均已校验':'本机备份已校验';}
|
||||
function renderBackup(value){
|
||||
$('#backup-run').disabled=value.running;$('#backup-result').textContent=value.running?(value.stage||'正在生成数据库副本…'):backupLabel(value.last)+(value.last?.error?':'+value.last.error:'');
|
||||
$('#backup-run').disabled=value.running;$('#backup-result').textContent=value.running?(value.stage||'正在生成数据库副本…'):backupLabel(value.last)+(value.last?.error?':'+value.last.error:'')+(value.last?.cloudCleanupError?':'+value.last.cloudCleanupError:'');
|
||||
$('#backup-local-path').textContent='本机目录:'+value.localPath;
|
||||
$('#backup-history').innerHTML=value.history.map(r=>`<article class="backup-row"><strong>${esc(backupLabel(r))}</strong><span>${esc(new Date(r.at).toLocaleString('zh-CN'))}</span><small>${esc(r.file||'')} ${r.bytes?Math.round(r.bytes/1024)+' KB':''}</small>${r.sha256?`<details><summary>完整性校验值</summary><code>${esc(r.sha256)}</code></details>`:''}</article>`).join('');
|
||||
$('#backup-history').innerHTML=value.history.map(r=>`<article class="backup-row"><strong>${esc(backupLabel(r))}</strong><span>${esc(new Date(r.at).toLocaleString('zh-CN'))}</span><small>${esc(r.file||'')} ${r.bytes?Math.round(r.bytes/1024)+' KB':''}</small>${r.baiduPath?`<small>百度网盘:${esc(r.baiduPath)}</small>`:''}${r.cloudCleanupOk?`<small>保留最近一个自然月,本次删除 ${Number(r.cloudDeleted||0)} 份过期备份</small>`:''}${r.cloudCleanupError?`<small>${esc(r.cloudCleanupError)}</small>`:''}${r.sha256?`<details><summary>完整性校验值</summary><code>${esc(r.sha256)}</code></details>`:''}</article>`).join('');
|
||||
clearTimeout(settingsTimer);if(value.running&&activePage==='settings')settingsTimer=setTimeout(pollBackup,2000);
|
||||
}
|
||||
async function pollBackup(){const generation=appearanceGeneration;try{const data=await api('/api/backups');if(generation===appearanceGeneration&&activePage==='settings')renderBackup(data);}catch(e){if(!e.stale&&generation===appearanceGeneration)$('#backup-result').textContent=e.message;}}
|
||||
|
||||
Reference in New Issue
Block a user