Files
ucvl-home-vision/web/personalization.js
T

122 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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;
const imageJobs=new Map();
function nextImageJob(key){const value=(imageJobs.get(key)?.value||0)+1;imageJobs.set(key,{value,pending:false});return value;}
function imageJobCurrent(key,value){return imageJobs.get(key)?.value===value;}
function imageJobPending(key){return !!imageJobs.get(key)?.pending;}
function invalidateBrandImages(){nextImageJob('logo');nextImageJob('icon');nextImageJob('import');}
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&&currentHome().kind!=='home'||!isAdmin()&&currentUser.familyAccess==='none'&&!['read','edit'].includes(currentUser.heritageAccess)))page='overview';
if(page!=='households'&&currentHome())showPage(page);
}
}
function resetAppearance(){
invalidateBrandImages();nextImageJob('cover');
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){
invalidateBrandImages();
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;}
if(typeof loadBaiduSettings==='function')loadBaiduSettings();
$('#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,job=nextImageJob(type);nextImageJob('import');imageJobs.get(type).pending=true;try{const image=await readImage(e.target.files[0],max);if(generation!==appearanceGeneration||!imageJobCurrent(type,job))return;if(type==='logo')brandLogo=image;else brandIcon=image;renderBrandImages();$('#brand-status').textContent='图片已准备,保存发布配置后使用。';}catch(e){if(generation===appearanceGeneration&&imageJobCurrent(type,job))$('#brand-status').textContent=e.message;}finally{if(imageJobCurrent(type,job))imageJobs.get(type).pending=false;}};
}
$('#brand-logo-reset').onclick=()=>{nextImageJob('logo');nextImageJob('import');brandLogo='';renderBrandImages();};$('#brand-icon-reset').onclick=()=>{nextImageJob('icon');nextImageJob('import');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();if(imageJobPending('logo')||imageJobPending('icon')||imageJobPending('import')){$('#brand-status').textContent='图片或配置仍在读取,请完成后保存。';return;}const controls=[...e.currentTarget.elements],generation=appearanceGeneration;for(const control of controls)control.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{for(const control of controls)control.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;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 renderBackup(value){
$('#backup-run').disabled=value.running;$('#backup-result').textContent=value.running?(value.stage||'正在生成数据库副本…'):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,baidu:f.elements.baidu?.checked||false,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;}}};