8cea7dee20
- SiteForge Enterprise V8.10.0-dev.1 upstream source (210 tests) - Fix: v8_workflows._mapped_input now reads both 类型/分类 columns so WXR imports keep post/product types instead of collapsing to page - Docs: trade-pro theme port spec (R3) and implementation plan Co-Authored-By: Claude Code <noreply@anthropic.com>
60 lines
11 KiB
JavaScript
60 lines
11 KiB
JavaScript
'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='<div class="panel"><p class="muted">正在读取站点工作数据...</p></div>';
|
||
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 `<div class="panel-title"><div><h3>${E(name)}</h3><p>${E(caption)}</p></div>${actions}</div>`},
|
||
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=`<div class="panel">${this.section('全站设计规范','同一个主题的不同网站可拥有完全不同的字体、间距、圆角和排版节奏。')}<form id="tokensForm" onsubmit="EnterpriseUI.saveDesign(event)"><div class="setting-grid">${fields.map(([k,name,type,options])=>`<label>${E(name)}${type==='select'?`<select name="${k}">${options.map(o=>`<option ${o===x[k]?'selected':''} value="${o}">${o}</option>`).join('')}</select>`:`<input type="number" name="${k}" step="${k==='heading_scale'?'0.05':'1'}" value="${ATTR(x[k])}">`}</label>`).join('')}</div><div class="panel-footer"><button class="btn primary" type="submit">保存设计规范</button></div></form></div>`;
|
||
},
|
||
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?`<span class="pill">${r.errors} 项 URL 错误</span>`:'<span class="pill green">无 URL 缺失</span>';
|
||
document.getElementById('tab-content').innerHTML=`<div class="panel">${this.section('旧站 URL / SEO 基线验收','迁移模式发布前必须建立旧站基线,杜绝静默漏页。',`<button class="btn light small" onclick="EnterpriseUI.snapshot()">从当前内容建立基线</button>`)}<div class="metrics-grid"><div class="metric"><small>旧 URL 清单</small><strong>${r.baseline_count}</strong></div><div class="metric"><small>当前页面</small><strong>${r.current_count}</strong></div><div class="metric"><small>URL 缺失</small><strong>${r.errors}</strong></div><div class="metric"><small>SEO 差异</small><strong>${r.warnings}</strong></div></div><div class="panel-footer" style="justify-content:space-between">${badge}<a class="btn light" href="${sfRoot}/sites/${state.siteId}/baseline/export.csv">下载旧站 URL 清单</a></div><p class="muted" style="margin-top:15px">注意:最可靠的做法是在导入旧站后、换模板和调整内容前建立基线。也可调用 API 批量导入旧站已知 URL 清单。</p><div style="max-height:420px;overflow:auto">${r.issues.map(x=>`<div class="tip ${x.severity==='error'?'warning':''}"><strong>${E(x.route)} · ${E(x.field)}</strong><div style="overflow-wrap:anywhere">${E(x.from)} → ${E(x.to)}</div></div>`).join('')||'<div class="tip">暂无 URL 或 SEO 差异。</div>'}</div></div>`;
|
||
},
|
||
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=`<div class="panel">${this.section('异步构建任务','后台 Worker 负责执行耗时构建,关闭浏览器也不影响任务。',`<button class="btn primary small" onclick="EnterpriseUI.queueBuild()">+ 提交异步构建</button>`)}<table class="sf-table" style="width:100%"><thead><tr><th>任务 ID</th><th>状态</th><th>尝试次数</th><th>创建时间</th><th>操作</th></tr></thead><tbody>${jobs.map(j=>`<tr><td><code>${E(j.id)}</code></td><td>${E(j.state)}${j.cancel_requested?' (取消中)':''}</td><td>${j.attempts}/${j.max_attempts}</td><td>${simpleDate(j.created_at)}</td><td>${['queued','running','retrying'].includes(j.state)?`<button class="btn light tiny" onclick="EnterpriseUI.cancelJob('${j.id}')">取消</button>`:''}</td></tr>`).join('')||'<tr><td colspan="5">暂无任务</td></tr>'}</tbody></table></div><div class="panel" style="margin-top:22px">${this.section('构建版本与质量检测','检查 HTML / 站内链接 / 图片 ALT / SEO,再决定是否发布。')}<div class="setting-grid">${builds.slice(0,15).map(b=>`<div class="tip"><strong>${E(b.id)} · ${b.total_pages} 页</strong><p>${simpleDate(b.created_at)}</p><button class="btn light small" onclick="EnterpriseUI.quality('${b.id}')">质量检测</button> <button class="btn light small" onclick="EnterpriseUI.rollback('${b.id}')">发布 / 回滚至此版本</button></div>`).join('')||'<p>暂无构建,请先提交任务。</p>'}</div><div id="sf-quality-result"></div></div><div class="panel" style="margin-top:22px">${this.section('本地发布记录','发布记录保留全部构建版本,方便核对和恢复。')}${releases.slice(0,30).map(r=>`<p>${E(r.action)} · <code>${E(r.build_id)}</code> · ${simpleDate(r.created_at)}</p>`).join('')||'<p>暂无发布历史</p>'}</div>`;
|
||
},
|
||
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=`<div class="tip ${r.errors?'warning':''}"><h4>检查结果 · ${r.files} 页 · ${r.errors} 错误 · ${r.warnings} 警告</h4>${r.issues.slice(0,60).map(i=>`<p><strong>${E(i.file)}</strong> ${E(i.type)} ${E(i.target||'')}</p>`).join('')||'通过基础静态质量检查'}</div>`}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=`<div class="panel">${this.section('网站团队与角色','Owner 可以管理成员,Editor 可以修改内容,Reviewer 和 Viewer 为只读角色。')}<div class="setting-grid">${members.map(m=>`<div class="tip"><strong>${E(m.display_name||m.username)}</strong><p>${E(m.username)} · ${E(m.role)}</p></div>`).join('')||'<p>尚未添加团队成员。系统管理员拥有全部权限。</p>'}</div>${users.length?`<form onsubmit="EnterpriseUI.assign(event)" class="setting-grid"><label>已有账号<select name="user_id">${users.map(u=>`<option value="${u.id}">${E(u.username)}</option>`).join('')}</select></label><label>角色<select name="role"><option>owner</option><option>editor</option><option>reviewer</option><option>viewer</option></select></label><div><button class="btn primary" type="submit">授予站点访问权限</button></div></form>`:''}</div><div class="panel" style="margin-top:22px">${this.section('操作审计日志','记录修改 API 的操作者、时间、路径与响应状态。')}<div style="max-height:450px;overflow:auto">${logs.map(l=>`<div class="tip"><strong>${E(l.actor)} · ${E(l.operation)} · ${l.status}</strong><p style="overflow-wrap:anywhere">${E(l.path)} · ${simpleDate(l.created_at)}</p></div>`).join('')||'<p>暂无操作日志</p>'}</div></div>`;
|
||
},
|
||
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('团队与权限','创建协作者,再为每个网站分配独立角色。')+`<div class="split"><div class="panel">${this.section('新增团队账号','至少12位密码,使用 scrypt 加盐哈希存储。')}<form onsubmit="EnterpriseUI.addUser(event)" class="setting-grid"><label>账号<input required name="username" placeholder="editor_01"></label><label>显示名称<input required name="display_name" placeholder="运营编辑"></label><label class="full">临时密码<input required type="password" name="password" minlength="12" autocomplete="new-password"></label><div><button class="btn primary">创建账号</button></div></form></div><div class="panel">${this.section('现有成员','站点权限可在各个网站的“成员 / 审计”中配置。')}${users.map(u=>`<div class="tip"><strong>${E(u.username)}</strong><p>${E(u.display_name)} · ${u.disabled?'停用':'启用'}</p></div>`).join('')||'<p>尚无协作者。</p>'}</div></div>`;
|
||
}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;
|