'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==='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])=>``).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=>`
${esc(backupLabel(r))}${esc(new Date(r.at).toLocaleString('zh-CN'))}${esc(r.file||'')} ${r.bytes?Math.round(r.bytes/1024)+' KB':''}${r.sha256?`
完整性校验值${esc(r.sha256)}
`:''}
`).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;}}};