feat: inline nav editor in workspace + styled type tabs

- 外观与设计: nav edit button opens modal directly in workspace,
  no page editor redirect needed; add/remove/reorder/edit nav items
  with save-draft + publish buttons
- 内容与页面: type tabs now use segmented-control design (active
  blue pill group with counts) instead of scattered buttons

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-28 11:45:16 +08:00
parent 5e8ae60dfe
commit 0225c270ea
3 changed files with 66 additions and 3 deletions
+16
View File
@@ -45,3 +45,19 @@
@media(max-width:620px){.sf-launch-hero{flex-direction:column;align-items:start}.sf-launch-step{padding:16px;gap:11px;grid-template-columns:32px minmax(0,1fr)}.sf-launch-controls{align-items:stretch;flex-direction:column}.sf-launch-field,.sf-launch-controls>.btn{width:100%;max-width:none}.sf-launch-check{align-items:start}}
.theme-preview.trade-pro{background:linear-gradient(125deg,#0b3d91,#3d6dc1);color:#e8f1fb}.site-icon.trade-pro{background:#e8f1fb;color:#0b3d91}
/* ── 内容类型 Tab(分段控件)── */
.type-tabs{display:flex;gap:0;border:1px solid var(--line);border-radius:10px;overflow:hidden;overflow-x:auto;margin-bottom:18px;background:#fff}
.type-tab{display:inline-flex;align-items:center;gap:6px;padding:10px 18px;font-size:13px;font-weight:650;color:#5c7083;background:none;border:0;border-right:1px solid var(--line);white-space:nowrap;transition:background .12s,color .12s;cursor:pointer}
.type-tab:last-child{border-right:0}
.type-tab:hover{background:#f4f7fb;color:#1e4089}
.type-tab.active{background:var(--blue);color:#fff}
.type-tab .count{font-size:11px;font-weight:700;background:rgba(0,0,0,.07);padding:2px 7px;border-radius:99px;line-height:1.3}
.type-tab.active .count{background:rgba(255,255,255,.2)}
/* ── 导航编辑弹窗 ── */
.nav-edit-row{display:grid;grid-template-columns:1fr 1fr auto auto auto;gap:8px;align-items:center;padding:8px 0;border-bottom:1px solid #eef1f6}
.nav-edit-row:last-child{border-bottom:0}
.nav-edit-row input{padding:8px 10px;font-size:13px;border:1px solid #d9e1ed;border-radius:7px}
.nav-edit-row .handle{color:#94a3b8;font-size:16px;cursor:grab}
.nav-edit-actions{display:flex;justify-content:flex-end;gap:9px;margin-top:20px;padding-top:16px;border-top:1px solid var(--line)}
.nav-add-btn{display:flex;align-items:center;gap:6px;padding:8px 14px;border:1px dashed #b8c8dc;border-radius:8px;color:var(--blue);font-size:13px;font-weight:600;cursor:pointer;background:#f8fafd;margin-top:12px;width:100%;justify-content:center}
.nav-add-btn:hover{background:#edf3ff;border-color:var(--blue)}
+49 -3
View File
@@ -124,7 +124,7 @@ function content(){if(!currentSite())return noSite();let arr=state.entries.filte
const pageItems=arr.slice(start,start+PAGE_SIZE);
const typeCounts={};state.entries.forEach(e=>typeCounts[e.type]=(typeCounts[e.type]||0)+1);
const tabs=[{key:'',label:'全部',count:state.entries.length},...Object.entries(typeName).filter(([k])=>typeCounts[k]).map(([key,value])=>({key,label:value,count:typeCounts[key]}))];
const tabHtml=tabs.map(t=>`<button class=”btn btn-tiny ${(!state.typ&&t.key==='')||(state.typ===t.key)?'btn-primary':'btn-muted'}” style=”margin:0 4px 8px 0” data-action=”set-type-tab” data-type=”${t.key}”>${t.label} (${t.count})</button>`).join('');
const tabHtml=`<div class=”type-tabs” role=”tablist”>${tabs.map(t=>`<button class=”type-tab ${(!state.typ&&t.key==='')||(state.typ===t.key)?'active':''}” role=”tab” data-action=”set-type-tab” data-type=”${t.key}”>${t.label}<span class=”count”>${t.count}</span></button>`).join('')}</div>`;
const pagination=totalPages>1?`<div style=”display:flex;align-items:center;gap:12px;justify-content:center;padding:16px 0”><button class=”btn btn-tiny” data-action=”page-prev” ${state.page<=1?'disabled':''}>‹ 上一页</button><span class=”muted”>第 ${state.page} / ${totalPages} 页 · 共 ${arr.length} 条</span><button class=”btn btn-tiny” data-action=”page-next” ${state.page>=totalPages?'disabled':''}>下一页 ›</button></div>`:'';
return htmlHeading('内容与页面','所有网页、产品、车型和新闻都在这里维护,保存后重新构建即可更新网站。',actionButton('+ 添加内容','new-entry'))+
`<div class=”panel”><div class=”panel-head”><div><h2>内容列表</h2><p>共 ${state.entries.length} 条内容。点击”设计页面”可进行可视化排版。</p></div></div><div style=”margin-bottom:12px”>${tabHtml}</div><div class=”toolbar”><input id=”contentSearch” aria-label=”搜索内容” placeholder=”搜索标题或页面路径…” value=”${esc(state.search)}”></div>`+
@@ -134,7 +134,7 @@ function content(){if(!currentSite())return noSite();let arr=state.entries.filte
function design(){if(!currentSite())return noSite();const s=state.site;const cards=Object.entries(themeName).map(([id,name])=>`<div class="theme-card"><div class="theme-preview ${id}">${name}</div><div class="theme-desc"><h3>${name} ${s.theme===id?'<span class="pill success">当前使用</span>':''}</h3><p>${({automotive:'适合汽车出口、车型及品牌展示',industrial:'适合制造业、产品与技术参数',corporate:'适合集团公司、服务和案例',minimal:'适合作品展示和品牌形象','trade-pro':'适合外贸 B2B 英文站点与询盘'})[id]}</p><button class="btn btn-tiny ${s.theme===id?'btn-muted':'btn-primary'}" ${s.theme===id?'disabled':''} data-action="set-theme" data-theme="${id}">${s.theme===id?'正在使用':'使用此主题'}</button><a class="btn btn-tiny btn-muted" href="/theme-gallery?theme=${encodeURIComponent(id)}" target="_blank" rel="noopener">预览页面效果</a></div></div>`).join('');
return htmlHeading('外观与设计','先选一套网站风格,再打开可视化编辑器修改图文、导航和手机端布局。')+
`<div class="section-heading"><div><h2>第一步 · 选择行业主题</h2><p>更换主题不会删除您已经录入的业务内容和 URL。</p></div><a href="/theme-gallery" class="btn btn-tiny">查看完整主题预览 ↗</a></div><div class="choice-cards">${cards}</div>`+
`<div class="section-heading"><div><h2>第二步 · 导航菜单</h2><p>自定义网站的顶部导航:添加、删除、排序菜单项,支持三级下拉。</p></div></div><div class="panel" id="navQuickPanel"><div class="panel-head"><div><h2>☰ 网站导航菜单</h2><p>直接编辑菜单文字和链接,改完保存并重新构建即可生效。</p></div><a class="btn btn-primary" href="/editor/${encodeURIComponent(s.id)}/${encodeURIComponent((state.entries.find(e=>e.type==='page')||state.entries[0]||{id:''}).id)}" title="打开可视化编辑器中的全站设计面板">编辑导航菜单 →</a></div><p class="muted" style="padding:4px 0">点击后进入页面编辑器,选择右上角「全站页眉与页脚」即可修改导航。</p></div>`+
`<div class="section-heading"><div><h2>第二步 · 导航菜单</h2><p>自定义网站的顶部导航:添加、删除、排序菜单项,支持三级下拉。</p></div></div><div class="panel" id="navQuickPanel"><div class="panel-head"><div><h2>☰ 网站导航菜单</h2><p>直接编辑菜单文字和链接,改完保存并重新构建即可生效。</p></div><button class="btn btn-primary" type="button" data-action="open-nav-editor">编辑导航菜单 →</button></div><div id="navQuickPreview" class="muted" style="padding:4px 0">点击按钮直接修改导航。</div></div>`+
`<div class="section-heading"><div><h2>第三步 · 设计网页</h2><p>挑选要修改的页面;可在设计器中编辑导航、页眉、页脚及各设备布局。</p></div></div><div class="panel"><div class="panel-head"><div><h2>选择要设计的页面</h2><p>修改先保存为草稿,正式发布设计后才会参与下一次静态构建。</p></div></div><div class="grid-2 grid-equal">${state.entries.filter(e=>e.type==='page').slice(0,8).map(e=>`<a class="site-card" href="/editor/${encodeURIComponent(s.id)}/${encodeURIComponent(e.id)}"><span class="site-icon">▤</span><span><h3>${esc(e.title)}</h3><p>${esc(e.route)}</p></span><span class="arrow">›</span></a>`).join('')||'<div class="muted">尚无页面,请先添加内容。</div>'}</div></div><div class="help-hint" style="margin-top:19px"><strong>提示</strong>:网站设置用于公司信息和域名;可视化编辑器用于调整页面外观。两者不会混在同一表单里。</div>`;
}
function imports(){if(!currentSite())return noSite();return htmlHeading('导入与迁移','根据您已有的资料选择操作方式:Excel 产品、WordPress 网站,或者外部 API / 数据库。')+
@@ -312,7 +312,7 @@ async function init(){try{const me=await api('/api/auth/me');state.csrf=me.csrf;
else {state.view=target==='sites'?'sites':'home';rerender();rememberView(true)}
}catch(e){notifyError(e);$('#mainContent').innerHTML=emptyState('后台加载失败',e.message,'<button class="btn btn-primary" onclick="location.reload()">重新加载</button>')}}
// Delegated UI interactions keep navigation and form actions centrally auditable.
document.addEventListener('click',async ev=>{const sidebar=ev.target.closest('.nav-item[data-view]');if(sidebar&&!sidebar.hasAttribute('data-action')){navigate(sidebar.dataset.view);return;}const control=ev.target.closest('[data-action]');if(!control)return;const action=control.dataset.action;try{if(action==='navigate')navigate(control.dataset.view);if(action==='language-refresh')await loadLanguage();if(action==='set-type-tab'){state.typ=control.dataset.type;state.page=1;rerender()}if(action==='page-prev'){state.page=Math.max(1,state.page-1);rerender()}if(action==='page-next'){state.page++;rerender()}if(action==='new-site')openCreateWizard();if(action==='select-site')await selectSite(control.dataset.id);if(action==='new-entry')openEntry();if(action==='edit-entry')openEntry(control.dataset.id);if(action==='set-theme')await setTheme(control.dataset.theme);if(action==='build-now')await buildNow();if(action==='refresh-builds')await refreshBuilds();if(action==='check-readiness')await checkReadiness();if(action==='cancel-job')await cancelJob(control.dataset.id);if(action==='report')await showReport(control.dataset.id);if(action==='release-preflight')await releasePreflight();if(action==='release-approval')await releaseApproval();if(action==='release-deploy')await releaseDeploy();if(action==='release-rollback')await releaseRollback(control.dataset.target,control.dataset.id);if(action==='launch-refresh')await launchRefresh();if(action==='launch-build')await buildNow();if(action==='launch-report')await launchReport(control.dataset.id);if(action==='launch-request-approval')await launchRequestApproval();if(action==='launch-preflight')await launchPreflight();if(action==='launch-deploy')await launchDeploy()}catch(e){notifyError(e)}});
document.addEventListener('click',async ev=>{const sidebar=ev.target.closest('.nav-item[data-view]');if(sidebar&&!sidebar.hasAttribute('data-action')){navigate(sidebar.dataset.view);return;}const control=ev.target.closest('[data-action]');if(!control)return;const action=control.dataset.action;try{if(action==='navigate')navigate(control.dataset.view);if(action==='language-refresh')await loadLanguage();if(action==='set-type-tab'){state.typ=control.dataset.type;state.page=1;rerender()}if(action==='page-prev'){state.page=Math.max(1,state.page-1);rerender()}if(action==='page-next'){state.page++;rerender()}if(action==='open-nav-editor')await openNavEditor();if(action==='new-site')openCreateWizard();if(action==='select-site')await selectSite(control.dataset.id);if(action==='new-entry')openEntry();if(action==='edit-entry')openEntry(control.dataset.id);if(action==='set-theme')await setTheme(control.dataset.theme);if(action==='build-now')await buildNow();if(action==='refresh-builds')await refreshBuilds();if(action==='check-readiness')await checkReadiness();if(action==='cancel-job')await cancelJob(control.dataset.id);if(action==='report')await showReport(control.dataset.id);if(action==='release-preflight')await releasePreflight();if(action==='release-approval')await releaseApproval();if(action==='release-deploy')await releaseDeploy();if(action==='release-rollback')await releaseRollback(control.dataset.target,control.dataset.id);if(action==='launch-refresh')await launchRefresh();if(action==='launch-build')await buildNow();if(action==='launch-report')await launchReport(control.dataset.id);if(action==='launch-request-approval')await launchRequestApproval();if(action==='launch-preflight')await launchPreflight();if(action==='launch-deploy')await launchDeploy()}catch(e){notifyError(e)}});
document.addEventListener('click',ev=>{const btn=ev.target.closest('[data-close]');if(btn)closeModal(btn.dataset.close)});
$('#siteForm').addEventListener('submit',saveSite);$('#entryForm').addEventListener('submit',saveEntry);
document.addEventListener('submit',ev=>{if(ev.target.id==='settingsForm')saveSettings(ev);if(ev.target.id==='importForm')importFile(ev);if(ev.target.id==='langGroupForm')submitLanguageGroup(ev);if(ev.target.id==='langJoinForm')submitLanguageJoin(ev);if(ev.target.id==='langUnitForm')submitLanguageUnit(ev);if(ev.target.id==='langLinkForm')submitLanguageLink(ev)});
@@ -328,4 +328,50 @@ $('#logoutBtn').addEventListener('click',async()=>{try{await api('/api/auth/logo
$('#menuToggle').addEventListener('click',()=>{const open=$('#sideMenu').classList.toggle('open');$('#mobileBackdrop').classList.toggle('open',open);$('#menuToggle').setAttribute('aria-expanded',String(open))});
$('#mobileBackdrop').addEventListener('click',closeMobile);
document.addEventListener('keydown',ev=>{if(ev.key==='Escape')closeMobile()});
// ── 导航编辑器 ──
let navItems=[],navDesignRevision=0,navSiteDesign=null;
async function openNavEditor(){
const s=state.site;if(!s)return;
try{
const d=await api(`/api/sites/${s.id}/site-design`);
navSiteDesign=d.config||{};navDesignRevision=d.revision||0;
navItems=structuredClone(navSiteDesign.nav||[]);
$('#navModal').showModal();drawNavEditor();
}catch(e){toast('读取导航失败:'+e.message,true)}
}
function drawNavEditor(){
const el=$('#navEditorBody');
el.innerHTML=navItems.length?navItems.map((n,i)=>`
<div class="nav-edit-row">
<span class="handle" title="拖动排序">⋮⋮</span>
<input value="${esc(n.title||'')}" placeholder="菜单名称" oninput="navItems[${i}].title=this.value">
<input value="${esc(n.route||'/')}" placeholder="/路径/" oninput="navItems[${i}].route=this.value">
<button class="btn btn-tiny" title="上移" onclick="W.moveNav(${i},-1)" ${i===0?'disabled':''}>↑</button>
<button class="btn btn-tiny" title="下移" onclick="W.moveNav(${i},1)" ${i===navItems.length-1?'disabled':''}>↓</button>
<button class="btn btn-tiny" title="删除" onclick="W.removeNav(${i})" style="color:#c14f45">✕</button>
</div>`).join('')
:'<p class="muted" style="text-align:center;padding:28px 0">暂无菜单项,点击下方按钮添加。</p>';
}
async function saveNavDraft(){
const s=state.site;if(!s||!navSiteDesign)return;
try{
navSiteDesign.nav=navItems;
const r=await api(`/api/sites/${s.id}/site-design`,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({config:navSiteDesign,expected_revision:navDesignRevision})});
navDesignRevision=r.revision||navDesignRevision+1;
toast('导航草稿已保存,点击「发布导航」使其参与下一次构建');
}catch(e){toast('保存失败:'+e.message,true)}
}
async function publishNav(){
const s=state.site;if(!s||!navSiteDesign)return;
try{
await saveNavDraft();
await api(`/api/sites/${s.id}/site-design/publish`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({expected_revision:navDesignRevision})});
toast('导航已发布!重新构建网站后生效。');
$('#navModal').close();
}catch(e){toast('发布失败:'+e.message,true)}
}
function addNavItem(){navItems.push({title:'新菜单',route:'/'});drawNavEditor()}
function removeNav(i){navItems.splice(i,1);drawNavEditor()}
function moveNav(i,dir){const t=navItems[i];navItems[i]=navItems[i+dir];navItems[i+dir]=t;drawNavEditor()}
window.W={addNavItem,removeNav,moveNav,saveNavDraft,publishNav};
document.addEventListener('DOMContentLoaded',init);
+1
View File
@@ -57,4 +57,5 @@
<div class="modal-actions"><button type="button" data-close="siteModal" class="btn btn-muted">取消</button><button type="button" id="createBack" class="btn btn-muted" hidden>上一步</button><button type="button" id="createNext" class="btn btn-primary">下一步:选择风格 →</button><button type="submit" id="createSubmit" class="btn btn-primary" hidden>确认创建网站</button></div>
</form></dialog>
<dialog id="entryModal" class="modal"><form id="entryForm" class="modal-inner"><div class="modal-top"><div><p class="overline">SITE CONTENT</p><h2 id="entryModalTitle">添加内容</h2><p class="subtle">按业务填写信息即可。详细布局可以进入可视化编辑器修改。</p></div><button type="button" data-close="entryModal" class="x-btn" aria-label="关闭">×</button></div><input type="hidden" name="entry_id"><div class="form-fields"><label>内容类型<select name="type"><option value="page">网站页面</option><option value="product">产品</option><option value="vehicle">车型</option><option value="post">新闻文章</option><option value="brand">品牌</option><option value="case">案例</option><option value="service">服务项目</option></select></label><label>状态<select name="status"><option value="published">可参与网站构建</option><option value="draft">内容草稿</option></select></label><label class="full">标题<input name="title" required maxlength="250" placeholder="输入页面或产品标题"></label><label class="full">页面地址 <span class="optional">新内容可留空</span><input name="route" placeholder="/products/example/"></label><label class="full">简短介绍<textarea name="summary" rows="2" placeholder="显示在列表和摘要中的介绍"></textarea></label><label class="full">正文内容 <span class="optional">可选</span><textarea name="body" rows="5" placeholder="填写业务正文,复杂排版请使用可视化设计器"></textarea></label><details class="form-expand full"><summary>SEO 设置(进阶)</summary><div class="form-fields"><label class="full">SEO 标题<input name="seo_title" placeholder="默认使用内容标题"></label><label class="full">Meta 描述<textarea name="seo_description" rows="2"></textarea></label></div></details></div><div class="modal-actions"><button type="button" data-close="entryModal" class="btn btn-muted">取消</button><button type="submit" class="btn btn-primary">保存内容</button></div></form></dialog>
<dialog id="navModal" class="modal" style="width:640px"><div class="modal-inner"><div class="modal-top"><div><p class="overline">NAVIGATION</p><h2>☰ 编辑导航菜单</h2><p class="subtle">直接修改菜单文字和链接。保存后需重新构建网站才能生效。</p></div><button type="button" data-close="navModal" class="x-btn" aria-label="关闭">×</button></div><div id="navEditorBody" style="min-height:120px"><p class="muted">加载中…</p></div><button type="button" class="nav-add-btn" onclick="W.addNavItem()">+ 添加菜单项</button><div class="nav-edit-actions"><button type="button" data-close="navModal" class="btn btn-muted">取消</button><button type="button" class="btn" onclick="W.saveNavDraft()">保存草稿</button><button type="button" class="btn btn-primary" onclick="W.publishNav()">发布导航</button></div></div></dialog>
</body></html>