'use strict'; // Enterprise pages extend the existing reliable Docker Web console without requiring a CDN. tabs.push(['design-system','设计规范'],['migration-qa','迁移验收'],['operations','任务 / 版本'],['permissions','成员 / 审计']); const sfEnterpriseApi=async (path,method='GET',data)=>api(path,method==='GET'?{}:json(method,data)); const sfRoot='/api/enterprise'; const _oldRenderTab=App.renderTab.bind(App); App.renderTab=function(){ if(!['design-system','migration-qa','operations','permissions'].includes(state.tab))return _oldRenderTab(); const el=document.getElementById('tab-content'); el.innerHTML='

正在读取站点工作数据...

'; if(state.tab==='design-system')EnterpriseUI.design().catch(onError); if(state.tab==='migration-qa')EnterpriseUI.migration().catch(onError); if(state.tab==='operations')EnterpriseUI.operations().catch(onError); if(state.tab==='permissions')EnterpriseUI.permissions().catch(onError); }; const EnterpriseUI={ section(name,caption='',actions=''){return `

${E(name)}

${E(caption)}

${actions}
`}, async design(){ const design=await sfEnterpriseApi(`/api/sites/${state.siteId}/site-design`);const x=design.config.tokens; const fields=[['font','全站字体','select',['system','serif','modern']],['heading_scale','标题比例','number'],['radius','圆角半径(px)','number'],['section_spacing','板块上下间距(px)','number'],['container_width','内容最大宽度(px)','number'],['button_style','按钮形状','select',['rounded','pill','square']],['header_style','导航密度','select',['standard','compact']]]; document.getElementById('tab-content').innerHTML=`
${this.section('全站设计规范','同一个主题的不同网站可拥有完全不同的字体、间距、圆角和排版节奏。')}
${fields.map(([k,name,type,options])=>``).join('')}
`; }, async saveDesign(event){event.preventDefault();try{ const raw=Object.fromEntries(new FormData(event.target)); for(const k of ['heading_scale','radius','section_spacing','container_width'])raw[k]=Number(raw[k]); const path=`/api/sites/${state.siteId}/site-design`;const design=await sfEnterpriseApi(path);design.config.tokens=raw;await sfEnterpriseApi(path,'PUT',{config:design.config,expected_revision:design.revision}); notice('设计规范已保存为草稿;进入可视化编辑器的全站设计面板发布后才能用于构建'); }catch(err){onError(err)}}, async migration(){ const r=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/baseline/report`); const badge=r.errors?`${r.errors} 项 URL 错误`:'无 URL 缺失'; document.getElementById('tab-content').innerHTML=`
${this.section('旧站 URL / SEO 基线验收','迁移模式发布前必须建立旧站基线,杜绝静默漏页。',``)}
旧 URL 清单${r.baseline_count}
当前页面${r.current_count}
URL 缺失${r.errors}
SEO 差异${r.warnings}

注意:最可靠的做法是在导入旧站后、换模板和调整内容前建立基线。也可调用 API 批量导入旧站已知 URL 清单。

${r.issues.map(x=>`
${E(x.route)} · ${E(x.field)}
${E(x.from)} → ${E(x.to)}
`).join('')||'
暂无 URL 或 SEO 差异。
'}
`; }, async snapshot(){if(!confirm('这会用当前站点的 URL 与 SEO 覆盖旧基线。推荐在旧站导入完成后、进行改版前操作。是否继续?'))return; try{const r=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/baseline/snapshot`,'POST',{});notice(`已记录 ${r.captured} 个原始页面`);await this.migration()}catch(err){onError(err)}}, async operations(){ const jobs=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/jobs`); const releases=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/releases`); const builds=state.builds; document.getElementById('tab-content').innerHTML=`
${this.section('异步构建任务','后台 Worker 负责执行耗时构建,关闭浏览器也不影响任务。',``)}${jobs.map(j=>``).join('')||''}
任务 ID状态尝试次数创建时间操作
${E(j.id)}${E(j.state)}${j.cancel_requested?' (取消中)':''}${j.attempts}/${j.max_attempts}${simpleDate(j.created_at)}${['queued','running','retrying'].includes(j.state)?``:''}
暂无任务
${this.section('构建版本与质量检测','检查 HTML / 站内链接 / 图片 ALT / SEO,再决定是否发布。')}
${builds.slice(0,15).map(b=>`
${E(b.id)} · ${b.total_pages} 页

${simpleDate(b.created_at)}

`).join('')||'

暂无构建,请先提交任务。

'}
${this.section('本地发布记录','发布记录保留全部构建版本,方便核对和恢复。')}${releases.slice(0,30).map(r=>`

${E(r.action)} · ${E(r.build_id)} · ${simpleDate(r.created_at)}

`).join('')||'

暂无发布历史

'}
`; }, async queueBuild(){try{const j=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/jobs`,'POST',{kind:'build'});notice('构建任务已提交 '+j.id);await this.operations()}catch(err){onError(err)}}, async cancelJob(id){if(!confirm('确认取消任务 '+id+'?'))return;try{await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/jobs/${id}/cancel`,'POST',{});notice('已发送取消请求');await this.operations()}catch(err){onError(err)}}, async quality(id){try{const r=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/builds/${id}/quality`);const el=document.getElementById('sf-quality-result');if(el)el.innerHTML=`

检查结果 · ${r.files} 页 · ${r.errors} 错误 · ${r.warnings} 警告

${r.issues.slice(0,60).map(i=>`

${E(i.file)} ${E(i.type)} ${E(i.target||'')}

`).join('')||'通过基础静态质量检查'}
`}catch(err){onError(err)}}, async rollback(id){if(!confirm('确认将此构建版本设为当前本地发布版本?迁移站发布前必须通过 URL 基线检查。'))return; try{const result=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/releases/${id}/rollback`,'POST',{});notice(result.published?'发布成功':'发布失败');await this.operations()}catch(err){onError(err)}}, async permissions(){ const logs=await sfEnterpriseApi(`${sfRoot}/audit?site_id=${state.siteId}&limit=50`); let members=[];try{members=await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/members`)}catch{} let users=[];try{users=await sfEnterpriseApi(`${sfRoot}/team/users`)}catch{} document.getElementById('tab-content').innerHTML=`
${this.section('网站团队与角色','Owner 可以管理成员,Editor 可以修改内容,Reviewer 和 Viewer 为只读角色。')}
${members.map(m=>`
${E(m.display_name||m.username)}

${E(m.username)} · ${E(m.role)}

`).join('')||'

尚未添加团队成员。系统管理员拥有全部权限。

'}
${users.length?`
`:''}
${this.section('操作审计日志','记录修改 API 的操作者、时间、路径与响应状态。')}
${logs.map(l=>`
${E(l.actor)} · ${E(l.operation)} · ${l.status}

${E(l.path)} · ${simpleDate(l.created_at)}

`).join('')||'

暂无操作日志

'}
`; }, async assign(event){event.preventDefault();try{await sfEnterpriseApi(`${sfRoot}/sites/${state.siteId}/members`,'PUT',Object.fromEntries(new FormData(event.target)));notice('权限已更新');await this.permissions()}catch(err){onError(err)}}, async team(){try{const users=await sfEnterpriseApi(`${sfRoot}/team/users`);state.siteId=null;topTitle('团队账号'); document.getElementById('view').innerHTML=hero('团队与权限','创建协作者,再为每个网站分配独立角色。')+`
${this.section('新增团队账号','至少12位密码,使用 scrypt 加盐哈希存储。')}
${this.section('现有成员','站点权限可在各个网站的“成员 / 审计”中配置。')}${users.map(u=>`
${E(u.username)}

${E(u.display_name)} · ${u.disabled?'停用':'启用'}

`).join('')||'

尚无协作者。

'}
`; }catch(err){onError(err)}}, async addUser(event){event.preventDefault();try{await sfEnterpriseApi(`${sfRoot}/team/users`,'POST',Object.fromEntries(new FormData(event.target)));notice('团队账号创建成功');await this.team()}catch(err){onError(err)}} }; window.EnterpriseUI=EnterpriseUI;