114 lines
14 KiB
JavaScript
114 lines
14 KiB
JavaScript
'use strict';
|
||
const appearanceChoices={theme:{paper:'庭院书卷',clean:'清爽明亮',ink:'静夜墨色'},layout:{sidebar:'左侧目录',top:'顶部目录'},density:{comfortable:'舒展',compact:'紧凑'},backdrop:{mountains:'山水背景',plain:'纯色背景'},font:{serif:'宋体标题',sans:'简洁标题'},startPage:{households:'我的家庭与场所',overview:'场所总览',objects:'区域与设备',heritage:'家庭传承'}};
|
||
const appearanceLabels={theme:'配色主题',layout:'后台版式',density:'内容疏密',backdrop:'页面背景',font:'标题字体',startPage:'登录后首页'};
|
||
const initialDefaults={theme:'paper',layout:'sidebar',density:'comfortable',backdrop:'mountains',font:'serif',startPage:'households'};
|
||
let branding={name:'赵府',subtitle:'智能数字管家',motto:'赵氏家宅 · 起居有序',loginTitle:'欢迎回来',loginNote:'输入用户名和密码,进入家庭空间。',footer:'赵府 · 智能数字管家',logoUrl:'/zhao-icon.png',iconUrl:'/zhao-icon.png',defaults:initialDefaults};
|
||
let personalValues=Object.fromEntries(Object.keys(appearanceChoices).map(k=>[k,'default'])),personalRevision=0,appearanceOwner='',appearanceGeneration=0,settingsTimer=null;
|
||
let brandDraft=null,brandLogo='',brandIcon='',backupRevision=0,coverDraft;
|
||
pageInfo.settings=['设置中心','发布配置统一管理,每个账号保留自己的使用习惯。'];
|
||
function effectiveAppearance(values=personalValues,defaults=branding.defaults){return Object.fromEntries(Object.keys(appearanceChoices).map(k=>[k,appearanceChoices[k][values[k]]?values[k]:appearanceChoices[k][defaults?.[k]]?defaults[k]:initialDefaults[k]]));}
|
||
function applyAppearance(values=personalValues,defaults=branding.defaults){
|
||
const effective=effectiveAppearance(values,defaults);
|
||
for(const key of ['theme','layout','density','backdrop','font'])document.documentElement.dataset[key]=effective[key];
|
||
return effective;
|
||
}
|
||
function applyBranding(){
|
||
document.title=branding.name+' · '+branding.subtitle;
|
||
$('#brand-name').textContent=branding.name;$('#brand-subtitle').textContent=branding.subtitle;
|
||
$('#brand-motto').textContent=branding.motto;$('#brand-footer').textContent=branding.footer;
|
||
$('#brand-image').src=branding.logoUrl;$('#brand-image').alt=branding.name+'标识';
|
||
$('link[rel="icon"]').href=branding.iconUrl;$('link[rel="apple-touch-icon"]').href=branding.iconUrl;
|
||
if(!setup){$('#auth-title').textContent=branding.loginTitle;$('#auth-note').textContent=branding.loginNote;}
|
||
applyAppearance();
|
||
}
|
||
async function loadBranding(){try{branding=await api('/api/branding');applyBranding();}catch(e){if(!e.stale)applyBranding();}}
|
||
function onAppearanceSession(preferences){
|
||
applyBranding();if(!currentUser||!preferences)return;
|
||
const first=appearanceOwner!==currentUser.id;appearanceOwner=currentUser.id;
|
||
personalValues=preferences.values;personalRevision=preferences.revision;branding.defaults=preferences.defaults;applyAppearance();
|
||
if(first&&activePage==='settings'){loadSettings();return;}
|
||
if(first&&activePage==='households'){
|
||
let page=effectiveAppearance().startPage;
|
||
if(page==='heritage'&&(currentHome()?.kind&¤tHome().kind!=='home'||!isAdmin()&¤tUser.familyAccess==='none'&&!['read','edit'].includes(currentUser.heritageAccess)))page='overview';
|
||
if(page!=='households'&¤tHome())showPage(page);
|
||
}
|
||
}
|
||
function resetAppearance(){
|
||
appearanceGeneration++;clearTimeout(settingsTimer);settingsTimer=null;appearanceOwner='';brandDraft=null;brandLogo='';brandIcon='';coverDraft=undefined;
|
||
personalValues=Object.fromEntries(Object.keys(appearanceChoices).map(k=>[k,'default']));personalRevision=0;applyAppearance();
|
||
for(const selector of ['#brand-form','#preferences-form','#backup-form'])$(selector).reset();
|
||
$('#settings-private').hidden=true;$('#backup-result').replaceChildren();$('#backup-history').replaceChildren();
|
||
}
|
||
function appearanceFields(container,values,inherit){
|
||
container.innerHTML=Object.entries(appearanceChoices).map(([k,choices])=>`<label>${appearanceLabels[k]}<select name="${k}">${inherit?'<option value="default">跟随平台默认</option>':''}${Object.entries(choices).map(([value,label])=>`<option value="${value}">${label}</option>`).join('')}</select></label>`).join('');
|
||
for(const k of Object.keys(appearanceChoices))container.querySelector(`[name="${k}"]`).value=values[k];
|
||
}
|
||
function fillBrandForm(value){
|
||
brandDraft=value;brandLogo=value.logoData||'';brandIcon=value.iconData||'';
|
||
const form=$('#brand-form');form.reset();
|
||
for(const k of ['name','subtitle','motto','loginTitle','loginNote','footer'])form.elements[k].value=value[k];
|
||
appearanceFields($('#brand-defaults'),value.defaults,false);renderBrandImages();
|
||
}
|
||
function renderBrandImages(){
|
||
$('#brand-logo-preview').src=brandLogo||'/zhao-icon.png';$('#brand-icon-preview').src=brandIcon||'/zhao-icon.png';
|
||
}
|
||
async function loadSettings(){
|
||
const generation=++appearanceGeneration;$('#settings-error').textContent='正在读取…';$('#settings-private').hidden=true;
|
||
try{
|
||
const [preferences,config,backup]=await Promise.all([api('/api/preferences'),isPlatformAdmin()?api('/api/appearance'):null,isPlatformAdmin()?api('/api/backups'):null]);
|
||
if(generation!==appearanceGeneration||activePage!=='settings')return;
|
||
personalValues=preferences.values;personalRevision=preferences.revision;branding.defaults=preferences.defaults;
|
||
appearanceFields($('#personal-choices'),personalValues,true);applyAppearance();
|
||
if(config){fillBrandForm(config);fillBackup(backup);$('#settings-private').hidden=false;}
|
||
$('#settings-error').textContent='';
|
||
}catch(e){if(!e.stale&&generation===appearanceGeneration)$('#settings-error').textContent=e.message;}
|
||
}
|
||
function readAppearanceFields(container){return Object.fromEntries([...container.querySelectorAll('select')].map(el=>[el.name,el.value]));}
|
||
function leaveSettings(){appearanceGeneration++;clearTimeout(settingsTimer);settingsTimer=null;applyAppearance();}
|
||
$('#preferences-form').addEventListener('submit',async e=>{
|
||
e.preventDefault();const generation=appearanceGeneration,b=e.currentTarget.querySelector('[type=submit]');b.disabled=true;
|
||
try{const saved=await api('/api/preferences',{revision:personalRevision,values:readAppearanceFields($('#personal-choices'))});if(generation!==appearanceGeneration)return;
|
||
personalValues=saved.values;personalRevision=saved.revision;applyAppearance();$('#preferences-status').textContent='已保存到当前账号,重新登录及换设备后仍生效。';
|
||
}catch(e){if(!e.stale&&generation===appearanceGeneration)$('#preferences-status').textContent=e.message;}finally{b.disabled=false;}
|
||
});
|
||
$('#personal-choices').onchange=()=>applyAppearance(readAppearanceFields($('#personal-choices')));
|
||
$('#personal-follow').onclick=()=>{appearanceFields($('#personal-choices'),Object.fromEntries(Object.keys(appearanceChoices).map(k=>[k,'default'])),true);applyAppearance({});$('#preferences-status').textContent='已预览平台默认,点击保存后生效。';};
|
||
$('#personal-cancel').onclick=()=>{appearanceFields($('#personal-choices'),personalValues,true);applyAppearance();$('#preferences-status').textContent='已恢复已保存的偏好。';};
|
||
$('#settings-reload').onclick=loadSettings;
|
||
async function readImage(file,maxSide=512){
|
||
if(!file||!['image/png','image/jpeg','image/webp'].includes(file.type)||file.size>5*1024*1024)throw Error('请选择 5 MB 以内的 PNG、JPEG 或 WebP 图片');
|
||
const bitmap=await createImageBitmap(file);
|
||
try{if(bitmap.width*bitmap.height>40000000)throw Error('图片尺寸过大,请先缩小');
|
||
let scale=Math.min(1,maxSide/Math.max(bitmap.width,bitmap.height));const canvas=document.createElement('canvas'),limit=maxSide>256?2*1024*1024:256*1024;
|
||
for(let attempt=0;attempt<5;attempt++){
|
||
canvas.width=Math.max(1,Math.round(bitmap.width*scale));canvas.height=Math.max(1,Math.round(bitmap.height*scale));
|
||
canvas.getContext('2d').drawImage(bitmap,0,0,canvas.width,canvas.height);const result=canvas.toDataURL('image/png');
|
||
if((result.length-22)*.75<=limit)return result;scale*=.8;
|
||
}
|
||
throw Error('图片转换后仍过大,请选择较小的图片');
|
||
}finally{bitmap.close();}
|
||
}
|
||
for(const [id,type,max] of [['brand-logo-file','logo',256],['brand-icon-file','icon',96]]){
|
||
$('#'+id).onchange=async e=>{const generation=appearanceGeneration;try{const image=await readImage(e.target.files[0],max);if(generation!==appearanceGeneration)return;if(type==='logo')brandLogo=image;else brandIcon=image;renderBrandImages();$('#brand-status').textContent='图片已准备,保存发布配置后使用。';}catch(e){if(generation===appearanceGeneration)$('#brand-status').textContent=e.message;}};
|
||
}
|
||
$('#brand-logo-reset').onclick=()=>{brandLogo='';renderBrandImages();};$('#brand-icon-reset').onclick=()=>{brandIcon='';renderBrandImages();};
|
||
function brandPayload(){const form=$('#brand-form');return {...Object.fromEntries(['name','subtitle','motto','loginTitle','loginNote','footer'].map(k=>[k,form.elements[k].value])),defaults:readAppearanceFields($('#brand-defaults')),logoData:brandLogo,iconData:brandIcon,revision:brandDraft?.revision};}
|
||
$('#brand-form').addEventListener('submit',async e=>{
|
||
e.preventDefault();const b=e.currentTarget.querySelector('[type=submit]'),generation=appearanceGeneration;b.disabled=true;
|
||
try{const result=await api('/api/appearance',brandPayload());if(generation!==appearanceGeneration)return;fillBrandForm(result);await loadBranding();if(generation!==appearanceGeneration)return;$('#brand-status').textContent='发布配置已保存。已有个人偏好继续保留。';}
|
||
catch(e){if(!e.stale&&generation===appearanceGeneration)$('#brand-status').textContent=e.message;}finally{b.disabled=false;}
|
||
});
|
||
$('#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;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)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)$('#brand-status').textContent=e.message;}};
|
||
function fillBackup(value){const c=value.config,f=$('#backup-form');backupRevision=c.revision;f.elements.enabled.checked=c.enabled;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.networkOk?'本机和网络目录副本均已校验':'本机备份已校验';}
|
||
function renderBackup(value){
|
||
$('#backup-run').disabled=value.running;$('#backup-result').textContent=value.running?'正在生成数据库副本…':backupLabel(value.last)+(value.last?.error?':'+value.last.error:'');
|
||
$('#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('');
|
||
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;}}
|
||
$('#backup-form').addEventListener('submit',async e=>{e.preventDefault();const f=e.currentTarget,b=f.querySelector('[type=submit]'),generation=appearanceGeneration;b.disabled=true;try{const data=await api('/api/backups/config',{revision:backupRevision,enabled:f.elements.enabled.checked,time:f.elements.time.value,keep:Number(f.elements.keep.value),networkPath:f.elements.networkPath.value.trim()});if(generation!==appearanceGeneration)return;fillBackup(data);$('#backup-save-status').textContent='备份设置已保存。';}catch(e){if(!e.stale&&generation===appearanceGeneration)$('#backup-save-status').textContent=e.message;}finally{b.disabled=false;}});
|
||
$('#backup-run').onclick=async()=>{const generation=appearanceGeneration;$('#backup-run').disabled=true;try{await api('/api/backups/run',{});if(generation===appearanceGeneration)pollBackup();}catch(e){if(!e.stale&&generation===appearanceGeneration){$('#backup-result').textContent=e.message;$('#backup-run').disabled=false;}}};
|