Files
cms-SiteForge/app/static/professional-ui.js
T
浅念 5c7643fcf2 upgrade: adopt V8.12.0-dev.1 (V8.11 unified professional UI + V8.12 workflow usability)
- Overlay 26 changed files from SiteForge_Enterprise_V8_12 (UI layer only:
  professional-ui.js/css, workspace/factory-v2/operations/editor templates,
  +2 test files, compose/ci/scripts/docs)
- Re-apply v8_workflows type-mapping fix (类型/分类) lost in upgrade
- Pin compose project name to siteforge-v8-10 so existing data volumes
  (hesupipe site 483295c29ea0, 193 entries) keep attached
- Drop stray .s10_* state files; ignore .s10_*/.sf_*/.s87_* patterns
- Plan/spec revisions per plan-review round 1 (icons are new design,
  real test assertions, recorded deviations)

Tests: 226 passed / 1 env-only failure (test needs node binary in PATH,
absent in python:3.12-slim container; host node v24 --check all 9 JS OK)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 17:02:45 +08:00

68 lines
10 KiB
JavaScript
Raw 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.
/* Shared non-invasive shell for SiteForge specialized tools. No business API changes. */
(()=>{'use strict';
const PAGES={editor:{title:'可视化编辑器',group:'外观与设计',help:'编辑区块和全站布局,先保存草稿,确认后再发布设计。',more:'左侧调整页面结构,中间查看真实主题预览,右侧编辑属性。修改草稿不会改变已构建网站;发布设计后需要重新构建。',next:'完成设计后到「构建与交付」查看最终 HTML。'},theme:{title:'主题预览',group:'外观与设计',help:'比较四套主题及不同设备的页面效果。',more:'这里使用演示资料展示主题结构。选择真正的网站主题请返回工作台 → 外观与设计,切换后重新构建。',next:'返回外观与设计,选择主题并编辑页面。'},factory:{title:'高级建站',group:'专业工具',help:'主题版本、字段映射、SEO 审计及高级构建集中在这里。',more:'先选择网站,再按“总览 → 主题 → 内容 → 迁移检查 → 构建”逐步操作。普通内容管理请使用简洁工作台。',next:'数据准备完成后执行构建与质量检查。'},production:{title:'数据与迁移工具',group:'导入与迁移',help:'一次只处理一类数据:选工具 → 预览 → 审核 → 提交。',more:'WordPress、Excel/CSV、XML、API 与 MySQL 都进入可复核的暂存流程。高级参数可保留默认值,完成后回到工作台构建。',next:'在构建与交付中验证导入后的实际页面。'},operations:{title:'任务与交付',group:'构建与交付',help:'检查迁移问题、Worker 任务、构建质量和发布审批。',more:'按“迁移复核 → 构建任务 → 质量报告 → 审批 → 部署”顺序处理。部署操作会影响线上环境,请先核查版本和目标。',next:'通过质量与审批后,再选择目标环境发布。'}};
const ctx=document.body?.dataset.sfPage; if(!PAGES[ctx])return; const p=PAGES[ctx],siteKey='siteforge:selected-site';
const safeRead=()=>{try{return localStorage.getItem(siteKey)||''}catch{return ''}};
const safeWrite=id=>{try{if(id)localStorage.setItem(siteKey,id);else localStorage.removeItem(siteKey)}catch{}};
const route=(view)=>`/?view=${encodeURIComponent(view)}${selected()?`&site=${encodeURIComponent(selected())}`:''}`;
const selected=()=>document.getElementById('sfGlobalSite')?.value || safeRead();
const navbar=[['网站工作台','/','home'],['内容管理','content','content'],['外观设计','design','design'],['导入迁移','import','import'],['构建交付','build','build']];
const html=document.createElement('div');html.className='sf-globalbar';html.setAttribute('role','navigation');html.setAttribute('aria-label','网站工作区导航');
const main=document.createElement('div');main.className='sf-bar-main';
const brand=document.createElement('a');brand.href='/';brand.className='sf-bar-brand';brand.innerHTML='<span class="sf-bar-logo">SF</span><span>SiteForge</span>';
const loc=document.createElement('span');loc.className='sf-bar-loc';loc.textContent=`${p.group} / ${p.title}`;
const nav=document.createElement('nav');nav.className='sf-bar-nav';nav.setAttribute('aria-label','主要功能');
navbar.forEach(([label,view])=>{const a=document.createElement('a');a.textContent=label;a.dataset.sfView=view;a.href=view==='/'?'/':route(view);if((ctx==='editor'||ctx==='theme')&&view==='design'||ctx==='production'&&view==='import'||ctx==='operations'&&view==='build'){a.setAttribute('aria-current','page')}nav.append(a)});
const sLabel=document.createElement('label');sLabel.className='sf-bar-site';sLabel.textContent='当前网站';const sel=document.createElement('select');sel.id='sfGlobalSite';sel.setAttribute('aria-label','当前管理的网站');sel.append(new Option('正在加载…',''));sLabel.append(sel);
main.append(brand,loc,nav,sLabel);html.append(main);
const help=document.createElement('div');help.className='sf-onboarding';help.dataset.open='false';
const strong=document.createElement('strong');strong.textContent='当前任务';const short=document.createElement('span');short.textContent=p.help;const toggle=document.createElement('button');toggle.type='button';toggle.dataset.sfHelp='';toggle.textContent='查看操作说明 +';toggle.setAttribute('aria-expanded','false');
const more=document.createElement('div');more.className='sf-onboarding-more';more.textContent=`操作方法:${p.more} 下一步:${p.next}`;help.append(strong,short,toggle,more);
document.body.classList.add('sf-professional','sf-page-'+ctx);document.body.insertBefore(help,document.body.firstChild);document.body.insertBefore(html,help);
const syncHeight=()=>{document.documentElement.style.setProperty('--sf-extras-height',(html.getBoundingClientRect().height+help.getBoundingClientRect().height)+'px')};
const localSelect=()=>({factory:'#sitePicker',operations:'#site',production:'#site'})[ctx]||null;
const refreshNav=()=>nav.querySelectorAll('a[data-sf-view]').forEach(a=>{a.href=a.dataset.sfView==='/'?'/':route(a.dataset.sfView)});
const local=()=>document.querySelector(localSelect()||'#sf__no_local');
let applying=false;
function mirrorToLocal(id){const node=local();if(!node||!Array.from(node.options).some(o=>o.value===id))return false;if(node.value===id)return true;applying=true;node.value=id;node.dispatchEvent(new Event('change',{bubbles:true}));applying=false;return true}
function selectSite(id){safeWrite(id);refreshNav();}
window.SiteForgeShell={selectSite,selectedSite:selected};
sel.addEventListener('change',()=>{const id=sel.value;if(!id)return;selectSite(id);if(ctx==='editor'){location.assign(route('design'));return}if(!mirrorToLocal(id)&&ctx!=='theme'){location.assign(route('overview'))}});
nav.addEventListener('click',()=>selectSite(sel.value));
toggle.addEventListener('click',()=>{const on=help.dataset.open!=='true';help.dataset.open=String(on);toggle.textContent=on?'收起操作说明 −':'查看操作说明 +';toggle.setAttribute('aria-expanded',String(on));syncHeight()});
async function load(){try{const res=await fetch('/api/sites',{credentials:'same-origin'});if(!res.ok)throw Error('网站列表加载失败');const items=await res.json();sel.replaceChildren(new Option('选择网站',''));
for(const item of items){sel.append(new Option(item.name,item.id))}
const parts=location.pathname.split('/');const editorId=ctx==='editor'?parts[2]:'';const params=new URLSearchParams(location.search);const wanted=editorId||params.get('site')||safeRead()||items[0]?.id||'';
if(items.some(s=>s.id===wanted)){sel.value=wanted;safeWrite(wanted)}else if(items.length){sel.value=items[0].id;safeWrite(sel.value)}
const native=local();if(native){const apply=()=>{const desired=sel.value;if(Array.from(native.options).some(o=>o.value===desired))mirrorToLocal(desired)};if(native.options.length)apply();const observer=new MutationObserver(()=>{if(!applying)apply()});observer.observe(native,{childList:true});native.addEventListener('change',()=>{if(applying)return;sel.value=native.value;selectSite(native.value)})}
refreshNav();}catch(err){sel.replaceChildren(new Option('在工作台选择网站',''));sel.disabled=true}syncHeight()}
if(ctx==='operations'){
// Business steps are optional views over existing DOM; hidden panels retain their API listeners.
const content=document.querySelector('.layout > main');
const groups=Array.from(content?.querySelectorAll(':scope > .two')||[]);
const metrics=content?.querySelector('.metrics');
if(groups.length===3&&metrics){
const tabbar=document.createElement('div');tabbar.className='sf-ops-tabs';tabbar.setAttribute('role','tablist');tabbar.setAttribute('aria-label','运行与发布阶段');
const choices=[['migration','① 导入与复核','确认旧站数据'],['build','② 生成静态网站','查看 Worker 任务'],['release','③ 审批与发布','核对部署目标']];
const names=['migration','build','release'];groups.forEach((g,i)=>{g.dataset.sfOps=names[i];g.setAttribute('role','tabpanel')});
function choose(key){groups.forEach(g=>g.hidden=g.dataset.sfOps!==key);tabbar.querySelectorAll('button').forEach(b=>b.setAttribute('aria-selected',String(b.dataset.view===key)));}
choices.forEach(([key,title,desc])=>{const b=document.createElement('button');b.type='button';b.dataset.view=key;b.setAttribute('role','tab');const label=document.createElement('strong');label.textContent=title;const small=document.createElement('small');small.textContent=desc;b.append(label,small);b.addEventListener('click',()=>choose(key));tabbar.append(b)});
metrics.insertAdjacentElement('afterend',tabbar);choose('migration');
}
}
// Mark required and invalid fields visibly, leaving native validation intact.
document.addEventListener('invalid',ev=>{const field=ev.target;if(field.matches?.('input,select,textarea'))field.setAttribute('aria-invalid','true')},true);
document.addEventListener('input',ev=>{if(ev.target.matches?.('input,select,textarea')&&ev.target.checkValidity())ev.target.removeAttribute('aria-invalid')});
if(ctx==='editor'){
const studio=document.querySelector('.studio');const top=studio?.querySelector('.studio-top');
if(top){const reorder=(label,group,names)=>{const div=document.createElement('div');div.className='sf-editor-actiongroup';div.dataset.group=group;div.setAttribute('aria-label',label);for(const name of names){const node=Array.from(top.children).find(x=>x.tagName==='BUTTON' && (x.textContent||'').includes(name));if(node)div.append(node)}top.append(div)};
reorder('页面与全站设计','design',['设计规范','全站页眉']);reorder('历史与草稿','draft',['撤销','重做','放弃草稿','保存草稿']);reorder('预览与构建','preview',['刷新预览','构建静态站']);reorder('发布设计','publish',['发布设计']);
}
const tabs=document.createElement('div');tabs.className='sf-editor-mobile-tabs';tabs.setAttribute('role','tablist');tabs.setAttribute('aria-label','手机端编辑面板');[['structure','页面结构'],['preview','页面预览'],['properties','属性设置']].forEach(([view,title])=>{const b=document.createElement('button');b.type='button';b.setAttribute('role','tab');b.dataset.sfPane=view;b.textContent=title;b.addEventListener('click',()=>setPane(view));tabs.append(b)});
const body=studio?.querySelector('.studio-body');if(body)studio.insertBefore(tabs,body);
function setPane(pane){document.body.dataset.mobilePane=pane;tabs.querySelectorAll('button').forEach(b=>b.setAttribute('aria-selected',String(b.dataset.sfPane===pane)))}setPane('structure');
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',load);else load();
window.addEventListener('resize',syncHeight);
})();