chore: baseline V8.10.0-dev.1 with v8_workflows type-mapping fix and trade-pro spec

- 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>
This commit is contained in:
浅念
2026-10-22 16:49:25 +08:00
commit 8cea7dee20
354 changed files with 20736 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SiteForge · 智能网站工厂</title><link rel="stylesheet" href="/ui/admin.css"><script defer src="/ui/admin.js"></script><script defer src="/ui/enterprise-ui.js"></script></head><body>
<div class="workspace">
<aside class="sidebar"><div class="identity"><div class="logo">S<span>✳</span></div><div><strong>SiteForge</strong><small>WEBSITE FACTORY</small></div></div>
<div class="side-caption">工作空间</div><a class="side-link" href="/factory-studio" style="text-decoration:none;display:block">◈ &nbsp;网站生产中心 →</a><a class="side-link" href="/operations" style="text-decoration:none;display:block">⇄ &nbsp;迁移与企业发布中心 →</a><button class="side-link selected" id="nav-dashboard" onclick="App.openDashboard()"><span>▦</span> 网站项目</button><button class="side-link" onclick="App.openNewSite()"><span>⊕</span> 创建新网站</button><button class="side-link" onclick="EnterpriseUI.team()"><span>♙</span> 团队账号</button><button class="side-link" id="nav-template" onclick="App.openTemplates()"><span>▤</span> 模板资源库</button>
<div class="side-caption between">我的网站 <button title="新建网站" onclick="App.openNewSite()">+</button></div><div id="siteNav" class="site-nav"></div>
<div class="sidebar-bottom"><div class="status-dot"></div><div><strong>旧版管理界面 · 高级功能</strong><small>站点独立静态发布</small></div></div>
</aside>
<div class="shell"><header class="topbar"><div class="breadcrumb"><span>工作空间</span><span class="crumb-sep">/</span><strong id="topTitle">网站总览</strong></div><div class="top-actions"><span class="env-label">● ENTERPRISE EDITION</span><button class="btn light small" onclick="App.openTemplates()">浏览模板</button><button class="btn light small" onclick="App.logout()">退出登录</button><button class="btn primary small" onclick="App.openNewSite()">+ 创建网站</button></div></header>
<main class="main" id="view"><div class="loading">正在初始化 SiteForge...</div></main>
</div>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<dialog id="siteDialog" class="dialog"><form id="siteForm" method="dialog"><div class="dialog-head"><div><span class="eyebrow">CREATE WEBSITE</span><h2>创建全新网站</h2><p>选择模板并创建站点,后续可继续导入旧内容或添加新数据。</p></div><button type="button" class="icon-btn" onclick="App.closeDialog('siteDialog')">×</button></div><div class="form-grid"><label>网站名称 <input name="name" required maxlength="150" placeholder="例如:HESU Global"/></label><label>新网站域名 <input name="domain" placeholder="https://example.com(可稍后设置)"/></label><label class="full">网站简介 / 标语 <input name="tagline" placeholder="全球采购 · 本地服务 · 高效交付"/></label><label>建站模式 <select name="mode"><option value="new">新站搭建</option><option value="migration">旧站改版 / 迁移</option><option value="hybrid">混合建站</option></select></label><label>主题风格 <select name="theme" id="siteTheme"><option value="automotive">汽车与出行</option><option value="industrial">工业制造</option><option value="corporate" selected>企业品牌</option><option value="minimal">极简设计</option></select></label><label class="full">基于已保存模板 <select name="preset_id" id="presetSelector"><option value="">不使用自定义模板</option></select></label></div><div class="dialog-actions"><button class="btn light" type="button" onclick="App.closeDialog('siteDialog')">取消</button><button class="btn primary" type="submit">创建并进入工作台 →</button></div></form></dialog>
<dialog id="entryDialog" class="dialog wide"><form id="entryForm" method="dialog"><div class="dialog-head"><div><span class="eyebrow">CONTENT EDITOR</span><h2 id="entryHeading">新增页面 / 内容</h2><p>所有字段均属于当前网站,修改后重新构建即可更新静态文件。</p></div><button type="button" class="icon-btn" onclick="App.closeDialog('entryDialog')">×</button></div><input type="hidden" name="entry_id"><div class="form-grid"><label>内容类型 <select name="type" id="entryType" onchange="App.renderEntryFields()"><option value="page">页面 Page</option><option value="product">产品 Product</option><option value="post">文章 Post</option><option value="service">服务 Service</option><option value="case">案例 Case</option><option value="vehicle">车型 Vehicle</option><option value="brand">品牌 Brand</option><option value="other">自定义内容</option></select></label><label>发布状态 <select name="status"><option value="published">已发布</option><option value="draft">草稿</option></select></label><label class="full">标题 <input name="title" required placeholder="页面或产品名称"/></label><label class="full">URL 路径(不会自动更改既有 URL) <input name="route" placeholder="/product/example/"/><small>建议:/products/example/,若为迁移站必须填写原有路径</small></label><label class="full">摘要 <textarea name="summary" rows="2" placeholder="列表描述、首页标语等"></textarea></label><label class="full">内容 HTML <textarea name="body" rows="6" placeholder="<p>正文内容</p>"></textarea></label><label class="full">封面图片 URL <div class="inline-input"><input id="coverInput" name="cover" placeholder="media/xxx.jpg 或 https://..."/><button type="button" class="btn light small" onclick="document.querySelector('#coverFile').click()">上传图片</button><input id="coverFile" type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/avif" hidden onchange="App.uploadCover(this.files[0])"></div></label><label>SEO Title <input name="seo_title" placeholder="留空则使用页面标题"/></label><label>Canonical <input name="canonical" placeholder="默认根据域名与路径生成"/></label><label class="full">Meta Description <textarea name="seo_description" rows="2"></textarea></label><label>Robots <select name="robots"><option value="index,follow">index,follow</option><option value="noindex,follow">noindex,follow</option><option value="noindex,nofollow">noindex,nofollow</option></select></label><label>其他结构化字段 JSON <textarea name="meta" id="entryMeta" rows="4" placeholder='{"blocks": []}'></textarea></label><div id="dynamicContentFields" class="full"></div><div class="full editor-tools"><strong>首页区块快捷添加</strong><button type="button" class="btn light tiny" onclick="App.addBlock('hero')">+ Hero</button><button type="button" class="btn light tiny" onclick="App.addBlock('features')">+ 优势</button><button type="button" class="btn light tiny" onclick="App.addBlock('stats')">+ 数字统计</button><button type="button" class="btn light tiny" onclick="App.addBlock('cta')">+ CTA</button><small>基础区块使用 JSON 配置;进入「可视化页面设计」可进行拖动排序和属性调整。</small></div></div><div class="dialog-actions"><button type="button" class="btn light" onclick="App.closeDialog('entryDialog')">取消</button><button type="submit" class="btn primary">保存内容</button></div></form></dialog>
<a href="/production-v8" style="position:fixed;right:18px;bottom:20px;z-index:9900;background:#1658a6;color:white;padding:13px 18px;border-radius:30px;box-shadow:0 5px 24px #0003;text-decoration:none;font-size:13px">V8 网站生产控制台 →</a></body></html>
+71
View File
@@ -0,0 +1,71 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SiteForge Studio · 可视化建站</title><link rel="stylesheet" href="/ui/editor.css"><script defer src="/ui/editor.js"></script></head>
<body>
<div class="studio">
<header class="studio-top">
<div class="brand"><a href="/" title="返回网站管理">◈</a><div><b>SiteForge</b><small>VISUAL STUDIO</small></div></div>
<span class="divide"></span><div class="file-name"><span id="siteName">加载中</span><small> / </small><strong id="pageName">页面编辑器</strong></div>
<div class="top-center"><button id="deviceDesktop" class="icon-btn on" onclick="Studio.device('desktop')" title="PC预览">▣</button><button id="deviceTablet" class="icon-btn" onclick="Studio.device('tablet')" title="平板预览">▤</button><button id="devicePhone" class="icon-btn" onclick="Studio.device('phone')" title="手机预览">▯</button></div>
<button class="ui-btn ghost" onclick="Studio.undo()" title="撤销 Ctrl+Z">↶ 撤销</button><button class="ui-btn ghost" onclick="Studio.redo()" title="重做 Ctrl+Y">↷ 重做</button><button class="ui-btn ghost" onclick="Studio.showTokens()">设计规范</button><button class="ui-btn ghost" onclick="Studio.showSiteDesign()">全站页眉与页脚</button><span id="saveStatus" class="save-status">未修改</span><button class="ui-btn ghost" onclick="Studio.refreshPreview()">↻ 刷新预览</button><button class="ui-btn ghost" onclick="Studio.buildSite()">构建静态站</button><button class="ui-btn ghost" onclick="Studio.discardDraft()">放弃草稿</button><button class="ui-btn ghost" onclick="Studio.save()">保存草稿</button><button class="ui-btn primary" onclick="Studio.publishDesign()">发布设计 ↗</button>
</header>
<div class="studio-body">
<aside class="left-pane"><div class="pane-heading"><div><span class="overline">STRUCTURE</span><h3>页面结构</h3></div><span class="chip" id="blockCount">0 区块</span></div>
<div class="field"><label>页面</label><select id="pageSelect" onchange="Studio.choosePage(this.value)"></select></div>
<div class="field"><label>页面模板</label><select id="pageTemplate" onchange="Studio.changeTemplateKind(this.value)"></select></div>
<div style="padding:0 19px 7px"><button class="ui-btn ghost" style="width:100%" onclick="Studio.resetPreset()">◈ 应用此页面模板的预设区块</button></div>
<div class="field"><label>页面布局模式</label><select id="layoutMode" onchange="Studio.setLayoutMode(this.value)"><option value="append">保留主题原页面 + 追加区块</option><option value="replace">全页自由排版(保留导航与页脚)</option></select></div><div class="section-label">内容区块 · 拖拽可调整排序</div><div id="blockList" class="block-list"></div>
<div class="section-label">添加功能区块</div><div class="palette" id="palette"></div><div class="section-label">可复用组件库</div><button class="ui-btn ghost" style="margin:10px" onclick="Studio.saveReusable()">将当前区块存为组件</button><div class="palette" id="reusablePalette"></div>
<div class="sidebar-foot">页面由区块驱动,所见预览使用服务端与正式构建相同的数据绑定引擎。</div>
</aside>
<section class="preview-workspace"><div class="preview-head"><span class="live-dot"></span><span>实时预览</span><code id="routeDisplay">/</code><span class="preview-note">正在展示正式主题中的完整页面,草稿不会影响正式构建,发布设计后才会参与下次构建</span></div><div class="stage"><div class="device-frame" id="deviceFrame"><iframe id="sitePreview" title="页面预览" sandbox="allow-scripts" referrerpolicy="no-referrer"></iframe></div></div></section>
<aside class="right-pane"><div class="pane-heading"><div><span class="overline">PROPERTIES</span><h3>属性设置</h3></div><span class="chip" id="blockTypeLabel">页面</span></div>
<div class="inspector" id="inspector"><div class="no-selection">在左侧选择一个区块,在这里编辑它的文字、图片、布局及数据来源。</div></div>
<div class="right-foot"><div>动态数据绑定</div><p>支持 <code>{{site.name}}</code>、<code>{{page.title}}</code> 等变量;“动态内容列表”按类型自动查询当前网站的数据。</p></div>
</aside>
</div>
<dialog id="designModal" style="border:1px solid #e2e8f0;border-radius:14px;padding:24px;width:min(460px,90vw);box-shadow:0 16px 52px #142a492f"><h2>全站设计规范</h2><p>调整后保存并刷新预览,发布构建自动采用该规范。</p><form id="designForm" method="dialog" style="display:grid;gap:12px"><label>标题缩放 <input name="heading_scale" type="number" min="0.8" max="1.5" step="0.1"></label><label>圆角 <input name="radius" type="number" min="0" max="32"></label><label>版心宽度 <input name="container_width" type="number" min="880" max="1600"></label><label>板块间距 <input name="section_spacing" type="number" min="20" max="140"></label><label>字体 <select name="font"><option value="system">系统字体</option><option value="serif">衬线字体</option><option value="modern">现代字体</option></select></label><label>按钮风格 <select name="button_style"><option value="rounded">圆角</option><option value="square">直角</option><option value="pill">胶囊</option></select></label><label>导航风格 <select name="header_style"><option value="standard">标准</option><option value="compact">紧凑</option></select></label><div style="display:flex;gap:12px"><button class="ui-btn primary" type="button" onclick="Studio.saveTokens()">保存全站规范</button><button class="ui-btn ghost" value="cancel">取消</button></div></form></dialog>
<dialog id="siteDesignModal" style="border:1px solid #e2e8f0;border-radius:14px;padding:22px;width:min(750px,96vw);max-height:88vh;overflow:auto;box-shadow:0 18px 56px #142a492f">
<h2>全站设计 · 页眉、导航、页脚与响应式</h2>
<p style="color:#64748b">先保存独立草稿;预览只显示在编辑器内。发布后,所有页面的下次静态构建才会使用新设计。</p>
<div id="siteDesignStatus" role="status" style="font-weight:600;margin-bottom:12px">读取中...</div>
<form id="siteDesignForm" method="dialog" style="display:grid;gap:12px">
<fieldset style="display:grid;gap:9px;border:1px solid #e2e8f0;border-radius:8px"><legend>页眉与导航</legend>
<label><input type="checkbox" name="show_announcement"> 显示顶部提示栏</label>
<label>提示栏文字 <input name="announcement" maxlength="140" style="width:100%"></label>
<label><input type="checkbox" name="show_search"> 显示站内搜索</label>
<label><input type="checkbox" name="show_cta"> 显示联系按钮</label>
<label>按钮文字 <input name="cta_text" maxlength="40"></label>
<label>按钮路径 <input name="cta_href" placeholder="/contact/"></label>
<label>导航结构(JSON,每项含 title、route、可选 children)
<textarea name="nav" rows="6" style="display:block;width:100%;font-family:monospace" spellcheck="false"></textarea>
</label>
</fieldset>
<fieldset style="display:grid;gap:9px;border:1px solid #e2e8f0;border-radius:8px"><legend>全站页脚</legend>
<label>企业名称或页脚标题 <input name="footer_title" maxlength="120" style="width:100%"></label>
<label>页脚简介 <textarea name="footer_description" rows="2" maxlength="650" style="display:block;width:100%"></textarea></label>
<label>联系邮箱 <input name="footer_email" type="email" maxlength="150"></label>
<label><input name="footer_show_nav" type="checkbox"> 页脚显示导航链接</label>
</fieldset>
<fieldset style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;border:1px solid #e2e8f0;border-radius:8px"><legend>不同设备布局</legend>
<label>平板板块间距 <input name="tablet_gap" type="number" min="20" max="120"></label>
<label>手机板块间距 <input name="mobile_gap" type="number" min="16" max="100"></label>
<label>手机内容边距 <input name="mobile_container_padding" type="number" min="8" max="48"></label>
<label><input name="mobile_hide_announcement" type="checkbox"> 手机隐藏提示栏</label>
<label><input name="mobile_hide_footer_links" type="checkbox"> 手机隐藏页脚导航</label>
</fieldset>
<div style="display:flex;flex-wrap:wrap;gap:8px">
<button class="ui-btn ghost" type="button" onclick="Studio.previewSiteDesign()">预览草稿</button>
<button class="ui-btn ghost" type="button" onclick="Studio.discardSiteDesign()">放弃全站草稿</button>
<button class="ui-btn ghost" type="button" onclick="Studio.saveSiteDesign()">保存全站草稿</button>
<button class="ui-btn primary" type="button" onclick="Studio.publishSiteDesign()">发布全站设计</button>
<button class="ui-btn ghost" value="cancel">关闭</button>
</div>
</form>
</dialog>
<dialog id="siteMediaModal" style="border:1px solid #dce5f1;border-radius:14px;padding:24px;width:min(720px,94vw);max-height:85vh;overflow:auto">
<h2>本站图片素材库</h2><p style="font-size:13px;color:#66758a">选择图片后更新当前组件;保存草稿、发布设计后才会参与静态构建。</p>
<div id="siteMediaGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(135px,1fr));gap:12px"></div>
<button type="button" class="ui-btn ghost" onclick="document.querySelector('#siteMediaModal').close()" style="margin-top:15px">关闭</button>
</dialog>
<div id="toast" class="toast"></div>
</div></body></html>
+12
View File
@@ -0,0 +1,12 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SiteForge · 高级建站中心</title><link rel="stylesheet" href="/ui/factory-v2.css"><script defer src="/ui/factory-v2.js"></script></head>
<body><div class="shell">
<aside class="sidebar"><a class="identity" href="/factory-studio"><span class="mark">SF</span><span><strong>SiteForge</strong><small>高级建站工具</small></span></a>
<div class="side-title">生产中心 / WORKSPACE</div>
<nav id="navtabs"><button data-tab="overview" class="active">◫ &nbsp; 网站总览</button><button data-tab="themes">▧ &nbsp; 主题与版本</button><button data-tab="content">▤ &nbsp; 内容与数据装配</button><button data-tab="migration">⇄ &nbsp; 迁移与 SEO 验收</button><button data-tab="build">◈ &nbsp; 构建与发布</button></nav>
<div class="side-bottom"><a href="/operations">企业发布中心</a> · <a href="/theme-gallery">↗ 完整主题预览中心</a> · <a href="/">返回网站工作台</a><small>Docker · Static Sites · Multi Tenant</small></div></aside>
<div class="work"><header class="top"><div class="top-crumb">SiteForge / <span id="currentTab">网站总览</span></div><div class="top-controls"><label>当前网站 <select id="sitePicker" aria-label="当前网站"></select></label><button class="outline" id="reload">刷新</button><button class="primary" id="newSite">+ 新建网站</button></div></header>
<div class="content"><div id="alert" role="status"></div><div class="heading"><div><div class="eyebrow">MANAGE · ASSEMBLE · DEPLOY</div><h1 id="heading">网站生产工作台</h1><p id="description">将新内容或旧网站数据与主题组合,构建独立可部署的网站。</p></div><div class="head-actions"><a class="outline" id="openEditor" href="#">可视化编辑器 ↗</a><button class="primary" id="buildQuick">生成静态网站</button></div></div>
<section id="panel" aria-live="polite"></section></div>
</div></div>
<dialog id="createDialog"><form id="createForm" method="dialog"><h2>创建网站</h2><p>不需要旧网站,也能直接选择主题、添加内容、构建整套官网。</p><label>网站名称<input name="name" required placeholder="例如 Hesu Global"></label><label>域名<input name="domain" placeholder="https://example.com"></label><label>站点模式<select name="mode"><option value="new">全新网站</option><option value="migration">旧站迁移改版</option><option value="hybrid">混合建站</option></select></label><label>主题<select name="theme" id="newTheme"></select></label><div class="form-actions"><button type="button" class="outline" id="closeCreate">取消</button><button class="primary" type="submit">创建并初始化网站</button></div></form></dialog>
</body></html>
+5
View File
@@ -0,0 +1,5 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>登录 · SiteForge 网站工厂</title><style>
:root{font-family:Inter,system-ui,-apple-system,"Microsoft YaHei",sans-serif;color:#263952}*{box-sizing:border-box}body{margin:0;background:#f5f7fa;min-height:100vh;display:grid;place-items:center;padding:24px}main{width:min(430px,100%);background:#fff;border:1px solid #e1e8ef;border-radius:20px;box-shadow:0 25px 70px #19345512;padding:42px}.symbol{width:56px;height:56px;border-radius:15px;background:#173250;color:#fff;display:grid;place-items:center;font-size:26px;font-weight:700;margin-bottom:24px}h1{font-size:27px;margin:0 0 6px}p{font-size:13px;color:#7c8ca0;line-height:1.8;margin:0 0 29px}label{display:block;font-size:13px;font-weight:600;margin-bottom:9px}input{width:100%;height:48px;border:1px solid #d8e0e9;border-radius:10px;padding:0 14px;font-size:15px;outline:0}input:focus{border-color:#386aab;box-shadow:0 0 0 3px #417cbc1c}button{width:100%;margin-top:22px;background:#19446f;color:#fff;height:48px;border:0;border-radius:10px;font-size:14px;font-weight:650;cursor:pointer}button:disabled{opacity:.65}small{display:block;text-align:center;margin-top:23px;color:#96a4b6}#error{margin:12px 0 0;color:#b63d3d;min-height:18px;font-size:13px}
</style></head><body><main><div class="symbol">S✳</div><h1>欢迎使用 SiteForge</h1><p>登录网站生产工作台,管理网站、内容、主题和静态构建版本。</p><form id="auth"><label for="pass">账号密码</label><input id="username" type="text" value="admin" required autocomplete="username" placeholder="管理员或团队用户名"><label for="pass">管理员密码 / 团队密码</label><input id="pass" type="password" required autofocus autocomplete="current-password" placeholder="请输入部署时设置的密码"><div id="error" role="alert"></div><button id="submit" type="submit">登录工作台 →</button></form><small>SiteForge Web · Factory 7.1.0</small></main><script>
document.getElementById('auth').addEventListener('submit',async e=>{e.preventDefault();const b=document.getElementById('submit'),err=document.getElementById('error');b.disabled=true;err.textContent='';try{const res=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:document.getElementById('username').value,password:document.getElementById('pass').value})});if(!res.ok){const d=await res.json();throw new Error(d.detail||'登录失败')}location.replace('/')}catch(e){err.textContent=e.message;b.disabled=false}});
</script></body></html>
+10
View File
@@ -0,0 +1,10 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SiteForge · 企业运行中心</title><link rel="stylesheet" href="/ui/operations.css"><script defer src="/ui/operations.js"></script></head><body>
<div class="layout"><aside><a class="wordmark" href="/"><b>◆</b><span><strong>SiteForge</strong><small>高级运行工具</small></span></a><p class="eyebrow">网站生产工厂</p><a href="/">▦ 返回网站工作台</a><a href="/theme-gallery">▤ 主题预览</a><a class="active" href="/operations">⇄ 迁移与交付中心</a><div class="sidefoot">Web + Worker<br>PostgreSQL / Redis 可选部署</div></aside>
<main><header><div><span class="crumb">SiteForge / 企业运行中心</span><h1>迁移、构建与发布</h1><p>同一个界面核对迁移数据,执行静态构建、审批、发布与回滚。</p></div><div class="right"><label>网站项目<select id="site"></select></label><button id="reload" class="secondary">刷新</button></div></header>
<div id="notice" role="status"></div><div class="metrics"><div><span>站点构建</span><strong id="statBuild">—</strong></div><div><span>待处理任务</span><strong id="statJobs">—</strong></div><div><span>审批请求</span><strong id="statApprovals">—</strong></div><div><span>迁移模式</span><strong id="statMode">—</strong></div></div>
<div class="two"><section><div class="section-head"><h2>01 · CMS 数据迁移</h2><small>先预演,再写入网站数据库</small></div><div class="fields"><label>来源系统<select id="platform"><option value="wordpress-rest">WordPress REST JSON</option><option value="woocommerce">WooCommerce 产品 JSON</option><option value="drupal-jsonapi">Drupal JSON:API</option><option value="shopify">Shopify 商品 JSON</option></select></label><label>导出 JSON<input id="file" type="file" accept=".json,application/json"></label></div><div class="actions"><button id="previewImport">预演与查错</button><button class="secondary" id="commitImport" disabled>确认导入</button></div><div id="report" class="report">选择文件后,预演将显示内容数量、URL 错误和复杂字段待复核事项。</div></section>
<section><div class="section-head"><h2>02 · 导入风险复核</h2><small>处理插件字段与内容关系;迁移站上线前必须清零</small></div><div id="reviews" class="job-list">暂无待复核事项</div><p class="muted">复核人必须填写处理说明;仅更改记录状态不会自动修复旧站内容,请人工检查预览。</p></section></div>
<div class="two"><section><div class="section-head"><h2>03 · 生产构建任务</h2><small>由独立 Worker 处理</small></div><p class="muted">保存内容后提交任务。相同的构建请求会去重,构建可取消、重试或在超时后恢复。</p><div class="actions"><button id="build">提交异步构建</button><a id="edit" class="secondary link" href="#">打开网站编辑器 ↗</a></div><div id="jobList" class="job-list">加载中…</div></section></div>
<div class="two"><section><div class="section-head"><h2>04 · 构建质量与审批</h2><small>分权审核发布</small></div><div id="buildList" class="job-list"></div><h3>发布审批记录</h3><div id="approvals" class="job-list"></div></section><section><div class="section-head"><h2>05 · 远程发布</h2><small>HTTPS 签名发布 Agent</small></div><p class="muted">仅能部署已审批、质量检查通过的构建;目标服务器地址由管理员提前设置,不能在这里随意填写。</p><div class="fields"><label>目标环境<select id="target"><option value="staging">测试环境 staging</option><option value="production">生产环境 production</option></select></label><label>已构建版本<select id="releaseBuild"></select></label></div><div class="actions"><button id="deploy">发布所选版本</button><button class="secondary" id="refreshRelease">刷新部署历史</button></div><h3>已发布版本</h3><div id="releases" class="job-list">暂无部署记录</div></section></div>
<footer>企业运行与迁移中心 · 第三方 CMS 复杂插件、PostgreSQL/Redis 和公网部署应分别完成真实环境验收。</footer>
</main></div></body></html>
+161
View File
@@ -0,0 +1,161 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SiteForge · 数据与迁移工具</title>
<style>
:root{font-family:Inter,"Microsoft YaHei",system-ui,sans-serif;color:#202a39;background:#f4f7fb}*{box-sizing:border-box}body{margin:0}header{display:flex;align-items:center;justify-content:space-between;background:#101d31;color:white;padding:18px 28px;gap:16px}header h1{font-size:18px;margin:0}header small{color:#a4b9d4}main{max-width:1320px;margin:24px auto;padding:0 20px}.pill{border-radius:20px;background:#eaf2ff;color:#164a8d;font-size:12px;padding:5px 10px}.muted{color:#6d7685;font-size:13px}.row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.nav{display:flex;gap:10px}.nav a{color:#d8e8f9;font-size:13px;text-decoration:none}.kpi{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:18px 0}.kpi article,.panel{background:white;border:1px solid #e1e8f1;border-radius:12px;padding:20px;box-shadow:0 2px 12px #10203806}.kpi strong{display:block;font-size:25px;margin-top:8px}.layout{display:grid;grid-template-columns:1fr 1fr;gap:18px}.panel h2{font-size:17px;margin:0 0 6px}.panel h3{font-size:14px;margin:16px 0 8px}label{display:block;font-size:12px;color:#526075;font-weight:600;margin-top:13px;margin-bottom:5px}input,select,textarea{width:100%;padding:10px 11px;font:inherit;font-size:13px;border:1px solid #cbd6e6;border-radius:7px;background:white}textarea{min-height:115px;resize:vertical}button{cursor:pointer;border:1px solid #c5d2e4;border-radius:8px;background:white;color:#213249;font-weight:600;font-size:13px;padding:10px 14px}button:hover{border-color:#2274bd}button.primary{background:#1658a6;color:#fff;border-color:#1658a6}button.danger{color:#b22d2d}button:disabled{opacity:.45;cursor:not-allowed}hr{border:0;border-top:1px solid #e8edf5;margin:20px 0}.message{white-space:pre-wrap;font-size:12px;line-height:1.55;overflow:auto;max-height:270px;background:#f5f8fc;padding:12px;border-radius:8px}.results{max-height:270px;overflow:auto}.results>div{padding:10px;border-bottom:1px solid #eef1f5;font-size:13px;display:flex;gap:10px;justify-content:space-between}.foot{margin:20px 0;color:#6f7b8b;font-size:12px}.ok{color:#176d45}.err{color:#c0392b}@media(max-width:850px){.layout,.kpi{grid-template-columns:1fr}header{align-items:flex-start;flex-direction:column}main{margin:12px auto}}
[hidden]{display:none!important}.tool-switch{display:flex;flex-wrap:wrap;gap:9px;background:#fff;padding:12px;margin:18px 0 0;border-radius:12px;border:1px solid #e1e8f1}.tool-btn{background:#f6f8fc;color:#50627c;border-color:transparent;padding:11px 14px}.tool-btn.active{background:#e8f0ff;color:#174ea7;border:1px solid #a5c5f5}.tool-help{font-size:12px;color:#64748b;margin:12px 5px 16px}.advanced-layout{grid-template-columns:minmax(0,1fr)}.advanced-layout>.panel{width:100%}.nav a:first-child{border:1px solid #7793bd;padding:9px 11px;border-radius:7px;background:#fff1}@media(max-width:720px){.tool-switch{gap:5px}.tool-btn{padding:9px 11px}}
</style></head><body>
<header><div><h1>SiteForge · 网站生产控制台 <span style="font-size:11px;color:#acd5ff">V8.10 · 高级工具</span></h1><small>整站模板 · 数据批次导入 · 可审计回滚 · 静态构建</small></div><div class="nav"><a href="/">返回网站工作台</a><a href="/factory-studio">页面编辑器</a><a href="/theme-gallery">主题中心</a><a href="/operations">任务中心</a></div></header>
<main>
<div class="panel"><div class="row" style="justify-content:space-between"><div><h2>选择要操作的网站</h2><div class="muted">所有批次、模板和构建记录均按网站隔离;新网站可先在网站工作台创建。</div></div><select id="site" style="max-width:410px" aria-label="选择网站"></select></div></div>
<section class="kpi"><article><span class="muted">当前网站</span><strong id="siteName">—</strong></article><article><span class="muted">内容记录</span><strong id="entryCount">—</strong></article><article><span class="muted">导入批次</span><strong id="batchCount">—</strong></article></section>
<div class="tool-switch" role="tablist" aria-label="高级工具分类">
<button type="button" class="tool-btn active" data-tool="batch" role="tab" aria-selected="true">① 批次导入</button>
<button type="button" class="tool-btn" data-tool="templates" role="tab" aria-selected="false">② 整站模板</button>
<button type="button" class="tool-btn" data-tool="wp" role="tab" aria-selected="false">③ WordPress 迁移</button>
<button type="button" class="tool-btn" data-tool="api" role="tab" aria-selected="false">④ XML / API</button>
<button type="button" class="tool-btn" data-tool="mysql" role="tab" aria-selected="false">⑤ MySQL 数据源</button>
</div><p class="tool-help" id="toolHelp" role="status">先核对字段与 URL 冲突,再将数据正式写入网站。</p>
<section class="layout advanced-layout">
<div class="panel" id="tool-batch"><h2>批次数据导入</h2><div class="muted">先预览字段与 URL 冲突,确认后再提交;支持按批次回滚且保护导入后的人工作业。</div>
<label for="source">数据格式</label><select id="source"><option value="json">JSON 行数组</option><option value="wxr">WordPress WXR XML 文件</option><option value="table">Excel / CSV / JSON 文件</option></select>
<label for="kind">内容类型</label><select id="kind"><option value="product">产品</option><option value="vehicle">车型</option><option value="page">普通页面</option><option value="post">新闻文章</option><option value="case">案例</option><option value="brand">品牌</option><option value="service">服务</option></select>
<div id="jsonInputs"><label for="records">JSON 数据</label><textarea id="records" spellcheck="false">[{"车型名称":"Example Vehicle A","品牌":"Example Brand","动力形式":"EV"}]</textarea>
<div class="muted">第一行字段自动建议映射。中文字段可对应标题、品牌、分类、参数等。</div></div>
<div id="tableInputs" style="display:none"><label for="tableFile">上传 XLSX / CSV / JSON</label><input type="file" id="tableFile" accept=".xlsx,.csv,.json"></div><div id="wxrInputs" style="display:none"><label for="wxrFile">上传 WordPress 全量 WXR XML</label><input type="file" id="wxrFile" accept=".xml"></div>
<div class="row" style="margin-top:16px"><button class="primary" id="stage">1. 校验并暂存</button><button id="refreshBatch">刷新批次</button></div><pre class="message" id="importResult">请选择网站后导入。</pre>
<h3>已暂存/已导入批次</h3><div class="results" id="batches"></div>
</div>
<div class="panel" id="tool-templates" hidden><h2>整站模板生产线</h2><div class="muted">保存当前网站的页面区块、导航与设计令牌。模板不复制产品、文章、域名、Logo、SEO 或用户密钥。</div>
<label for="snapshotName">新模板名称</label><input id="snapshotName" value="企业官网整站模板"><button id="saveTemplate" style="margin-top:10px" class="primary">保存当前网站的模板快照</button>
<hr><label for="sourceSite">模板来源网站</label><select id="sourceSite"></select><label for="templateId">模板版本</label><select id="templateId"></select>
<div class="row" style="margin-top:14px"><button id="preflight">2. 兼容性预检</button><button id="apply" class="primary">3. 暂存整站模板</button><button id="publishTemplate" class="primary">4. Owner 发布模板</button></div>
<pre class="message" id="templateResult">先保存模板,再在另一网站上预检并应用。若存在同路径页面,需显式授权替换布局。</pre>
<hr><h3>静态网站生产</h3><div class="muted">使用现有独立 Worker 构建队列,提交后到任务中心检查超时、重试和取消。</div>
<div class="row" style="margin-top:12px"><button class="primary" id="build">提交静态构建任务</button><a href="/operations">查看任务进度 →</a></div>
<pre class="message" id="buildResult">尚未提交</pre>
</div>
</section>
<section class="panel" id="tool-wp" style="margin-top:18px" aria-labelledby="wpTitle" hidden>
<h2 id="wpTitle">WordPress 全站迁移工作台</h2>
<div class="muted">适用于你有权迁移的公开 WordPress 网站。每次安全读取一页(最多 50 条),保留进度,所有内容先暂存,确认后才生成原站 URL/SEO 基线。旧站复杂插件与页面效果仍须单独复核。</div>
<div class="row" style="align-items:flex-end;margin-top:12px">
<div style="flex:2;min-width:250px"><label for="wpOrigin">原 WordPress 网站地址</label><input id="wpOrigin" placeholder="https://old.example.com/"></div>
<div style="flex:1;min-width:140px"><label for="wpLimit">最多采集记录</label><input id="wpLimit" type="number" min="1" max="20000" value="2000"></div>
<button id="wpCreate" class="primary">新建迁移会话</button>
</div>
<div class="row" style="margin-top:10px">
<select aria-label="WordPress 迁移会话" id="wpSession" style="min-width:240px;max-width:430px"></select>
<button id="wpNext">继续采集一页</button><button id="wpWorker" class="primary">提交 Worker 后台采集</button><button id="wpSitemap">发现 Sitemap</button><button id="wpMedia">本地化图片(20 张)</button><select id="wpCatalogType" aria-label="同步 WordPress 目录"><option value="categories">分类</option><option value="tags">标签</option><option value="media">媒体库</option></select><button id="wpCatalogSync">同步目录下一页</button><button id="wpCatalogStatus">目录进度</button><button id="wpAuto">连续采集</button><button id="wpStop">停止连续采集</button>
<button id="wpFinalize" class="primary">确认并导入全部暂存</button>
<button id="wpCancel" class="danger">取消迁移</button>
<button id="wpReload">刷新状态</button><button id="wpAudit">对比最新构建的 URL/SEO</button>
</div>
<pre id="wpResult" class="message">选择站点并新建迁移会话。每页请求独立保存检查点;采集完成后可明确审核 URL、SEO、缺失内容。</pre>
</section>
<section class="panel" id="tool-api" style="margin-top:18px" aria-labelledby="p3Title" hidden>
<h2 id="p3Title">P3 · XML / HTTP API 数据连接器</h2>
<div class="muted">XML 仅支持扁平或两层记录,拒绝外部实体;HTTP API 支持公开或密钥引用认证的 HTTPS JSON。全部数据先进入暂存批次,需要单独审核并提交;API 采集在 Worker 后台执行。</div>
<div class="layout" style="margin-top:12px">
<div>
<h3>XML 导入</h3>
<label for="xmlFile">XML 文件(最大 8 MB)</label><input id="xmlFile" type="file" accept=".xml,text/xml,application/xml">
<label for="xmlRecord">记录节点标签(如 product / item)</label><input id="xmlRecord" value="item">
<label for="xmlMapping">字段映射 JSON(可为空自动识别)</label><textarea id="xmlMapping" spellcheck="false" rows="3">{}</textarea>
<button class="primary" id="xmlStage">校验并暂存 XML</button>
</div>
<div>
<h3>JSON API(公开或认证)</h3>
<label for="apiUrl">HTTPS 数据接口</label><input id="apiUrl" placeholder="https://api.example.com/v1/products">
<label for="apiAuth">认证方式</label><select id="apiAuth"><option value="none">公开 API</option><option value="bearer">Bearer Token</option><option value="api_key">X-API-Key</option><option value="custom">自定义 X- 请求头</option></select>
<label for="apiSecretRef">密钥引用(仅 Worker 读取,不填写密钥本身)</label><input id="apiSecretRef" placeholder="file:partner_api_token 或 env:SITEFORGE_API_TOKEN">
<label for="apiAuthHeader">自定义请求头(仅 custom)</label><input id="apiAuthHeader" value="X-Partner-Key">
<label for="apiItems">列表 JSON 路径</label><input id="apiItems" value="data.items">
<div class="row"><div style="flex:1"><label for="apiPageSize">每页数量</label><input id="apiPageSize" type="number" value="50" min="1" max="100"></div><div style="flex:1"><label for="apiMaxPages">最多页数</label><input id="apiMaxPages" type="number" value="10" min="1" max="50"></div></div>
<label for="apiMapping">字段映射 JSON(可为空自动识别)</label><textarea id="apiMapping" spellcheck="false" rows="3">{}</textarea>
<div class="row"><button class="primary" id="apiCreate">创建 API 数据源</button><button id="apiReload">刷新采集记录</button></div>
<label for="apiSource">已保存的数据源</label><select id="apiSource" aria-label="API 数据源"></select>
<div class="row"><button class="primary" id="apiRun">提交 Worker 采集</button><button id="apiStatus">查看采集与批次</button><button class="danger" id="apiCancel">取消采集</button></div>
</div>
</div>
<pre class="message" id="p3Result">创建数据源、提交 Worker 后到“任务中心”查看执行;采集完后到上方“已暂存批次”确认导入。</pre>
</section>
<section class="panel" id="tool-mysql" style="margin-top:18px" aria-labelledby="mysqlTitle" hidden>
<h2 id="mysqlTitle">P3 · 只读 MySQL 连接器</h2>
<div class="muted">需要管理员预先在服务器环境配置允许的主机和数据库表,并建立数据库侧仅 SELECT 的账号。强制 TLS 证书校验,拒绝任意 SQL;数据分批暂存,不自动发布。</div>
<div class="layout">
<div><label for="myHost">管理员批准的 MySQL 主机</label><input id="myHost" placeholder="db.internal.example">
<label for="myDatabase">数据库名</label><input id="myDatabase" placeholder="catalog">
<label for="myTable">表或视图</label><input id="myTable" placeholder="products_public">
<label for="myKey">有唯一索引的游标列</label><input id="myKey" value="id">
<label for="myColumns">允许读取的列(英文逗号分隔)</label><input id="myColumns" placeholder="id,name,brand,category">
</div>
<div><label for="myUserRef">MySQL 只读账号密钥引用</label><input id="myUserRef" placeholder="file:mysql_reader_user">
<label for="myPassRef">MySQL 密码密钥引用</label><input id="myPassRef" placeholder="file:mysql_reader_password">
<label for="myMapping">字段映射 JSON</label><textarea id="myMapping" rows="2">{"title":"name","brand":"brand"}</textarea>
<div class="row"><div style="flex:1"><label for="myPageSize">单页</label><input id="myPageSize" type="number" value="100" min="1" max="250"></div><div style="flex:1"><label for="myMaxPages">最多页数</label><input id="myMaxPages" type="number" value="10" min="1" max="50"></div></div>
<div class="row"><button class="primary" id="myCreate">创建 MySQL 数据源</button><button id="myReload">刷新</button></div>
<label for="mySource">已保存的数据源</label><select id="mySource" aria-label="MySQL 数据源"></select>
<div class="row"><button class="primary" id="myRun">提交后台采集</button><button id="myStatus">检查批次</button><button class="danger" id="myCancel">取消</button></div>
</div>
</div>
<pre class="message" id="myResult">配置源数据后交由 Worker 执行;使用最小权限账号、TLS CA 证书和服务器侧白名单。</pre>
</section>
<p class="foot">提示:V8 当前是分阶段开发版本。远程正式发布、Art Design Pro 官方工程编译、复杂 WordPress 组件无损迁移仍需专项验收。</p>
</main><script>
'use strict';const $=id=>document.getElementById(id);let sites=[],csrf='',templates=[];
const toolNames=['batch','templates','wp','api','mysql'];
const toolHelp={batch:'先核对字段与 URL 冲突,再将数据正式写入网站。',templates:'保存整站布局 → 预检 → 暂存 → Owner 确认应用。',wp:'逐页采集旧站、同步图片与分类;确认导入后进行 SEO 验收。',api:'先建立 XML 或 HTTPS API 数据源,执行后在“批次导入”中审核。',mysql:'此处面向管理员:只读账号、TLS 和数据源白名单必须事先配置。'};
function changeTool(key){if(!toolNames.includes(key))return;for(const k of toolNames){const el=$('tool-'+k);el.hidden=k!==key;}for(const btn of document.querySelectorAll('[data-tool]')){let yes=btn.dataset.tool===key;btn.classList.toggle('active',yes);btn.setAttribute('aria-selected',yes?'true':'false');btn.tabIndex=yes?0:-1;}$('toolHelp').textContent=toolHelp[key]||'';window.scrollTo({top:0,behavior:'instant'})}
document.querySelectorAll('[data-tool]').forEach(btn=>btn.addEventListener('click',()=>changeTool(btn.dataset.tool)));
async function api(path,method='GET',body){const opts={method,credentials:'same-origin',headers:{'Accept':'application/json'}};if(method!=='GET')opts.headers['X-SiteForge-CSRF']=csrf;if(body instanceof FormData)opts.body=body;else if(body!==undefined){opts.headers['Content-Type']='application/json';opts.body=JSON.stringify(body)}const r=await fetch(path,opts);let data;try{data=await r.json()}catch{data={detail:'服务器返回非 JSON'}}if(!r.ok)throw Error(typeof data.detail==='string'?data.detail:JSON.stringify(data.detail||data));return data}
function show(id,obj){$(id).textContent=typeof obj==='string'?obj:JSON.stringify(obj,null,2)}
function current(){return $('site').value}function opt(value,text){const el=document.createElement('option');el.value=value;el.textContent=text;return el}
async function load(){try{const me=await api('/api/auth/me');csrf=me.csrf;sites=await api('/api/sites');$('site').replaceChildren(...sites.map(s=>opt(s.id,s.name)));$('sourceSite').replaceChildren(...sites.map(s=>opt(s.id,s.name)));await refresh();}catch(e){alert('需要先登录或加载失败:'+e.message);location.href='/login'}}
async function refresh(){const s=sites.find(x=>x.id===current());if(!s)return;$('siteName').textContent=s.name;$('entryCount').textContent=s.entries_count;await loadBatches();await loadTemplates();await loadWpSessions();await loadApiSources();await loadMySources()}
async function loadBatches(){try{const data=await api(`/api/factory/sites/${current()}/staging`);$('batchCount').textContent=data.items.length;const box=$('batches');box.replaceChildren();for(const b of data.items){const div=document.createElement('div');const title=document.createElement('span');title.textContent=`${b.origin} · ${b.state} · ${b.input_count} 条`;div.append(title);const inspect=document.createElement('button');inspect.textContent='关系检查';inspect.onclick=async()=>{try{show('importResult',await api(`/api/connectors/sites/${current()}/import-batches/${b.id}/relationships`))}catch(e){show('importResult','关系检查失败:'+e.message)}};div.append(inspect);if(b.state==='staged'){const c=document.createElement('button');c.textContent='提交';c.onclick=()=>processBatch(b.id,'commit');div.append(c)}if(['committed','partial'].includes(b.state)){const c=document.createElement('button');c.textContent='回滚';c.className='danger';c.onclick=()=>processBatch(b.id,'rollback');div.append(c)}box.append(div)}}catch(e){show('importResult',e.message)}}
async function processBatch(id,action){let payload={};if(action==='commit'){const c=confirm('确认提交本批数据?遇到已存在的路径,默认跳过,不覆盖。');if(!c)return;payload={on_conflict:'skip'}}else if(!confirm('确认回滚本批导入?导入后被人工修改的内容将被保护,不能强制覆盖。'))return;try{show('importResult',await api(`/api/factory/sites/${current()}/staging/${id}/${action}`,'POST',payload));await loadBatches()}catch(e){show('importResult','错误:'+e.message)}}
$('source').onchange=()=>{$('jsonInputs').style.display=$('source').value==='json'?'block':'none';$('wxrInputs').style.display=$('source').value==='wxr'?'block':'none';$('tableInputs').style.display=$('source').value==='table'?'block':'none'};
$('site').onchange=refresh;$('sourceSite').onchange=loadTemplates;
$('stage').onclick=async()=>{try{let data;if($('source').value==='table'){const form=new FormData();if(!$('tableFile').files[0])throw Error('请选择 Excel/CSV/JSON 文件');form.append('file',$('tableFile').files[0]);form.append('content_type',$('kind').value);data=await api(`/api/factory/sites/${current()}/staging/file`,'POST',form)}else if($('source').value==='wxr'){const form=new FormData();if(!$('wxrFile').files[0])throw Error('请选择 WXR XML');form.append('file',$('wxrFile').files[0]);data=await api(`/api/factory/sites/${current()}/staging/wxr`,'POST',form)}else{const rows=JSON.parse($('records').value);data=await api(`/api/factory/sites/${current()}/staging`,'POST',{rows,content_type:$('kind').value})}show('importResult',data);await loadBatches()}catch(e){show('importResult','错误:'+e.message)}};
$('refreshBatch').onclick=loadBatches;
$('saveTemplate').onclick=async()=>{try{show('templateResult',await api(`/api/factory/sites/${current()}/whole-site-templates`,'POST',{name:$('snapshotName').value}));await loadTemplates()}catch(e){show('templateResult','错误:'+e.message)}};
async function loadTemplates(){const source=$('sourceSite').value;if(!source)return;try{templates=await api(`/api/factory/sites/${source}/whole-site-templates`);$('templateId').replaceChildren(...templates.map(t=>opt(t.id,`${t.name} · ${t.version}`)))}catch(e){show('templateResult',e.message)}}
async function applyTemplate(confirmApply){try{const payload={source_site_id:$('sourceSite').value,template_id:$('templateId').value,confirm:confirmApply,overwrite_layouts:confirmApply};if(!payload.template_id)throw Error('请先保存或选择整站模板');const document=await api(`/api/factory/sites/${payload.source_site_id}/whole-site-templates/${payload.template_id}`);const report=await api(`/api/factory/sites/${current()}/whole-site-templates/preflight`,'POST',{template:document});if(!report.compatible||!confirmApply){show('templateResult',report);return}if(!confirm('兼容性通过。应用模板将替换同路径页面布局,但保留内容及 SEO。确认?'))return;const draft=await api(`/api/factory/sites/${current()}/whole-site-templates/staged/current`);payload.expected_revision=draft.revision||0;show('templateResult',await api(`/api/factory/sites/${current()}/whole-site-templates/apply`,'POST',payload))}catch(e){show('templateResult','错误:'+e.message)}}
$('preflight').onclick=()=>applyTemplate(false);$('apply').onclick=()=>applyTemplate(true);$('publishTemplate').onclick=async()=>{try{const s=await api(`/api/factory/sites/${current()}/whole-site-templates/staged/current`);if(s.state!=='draft')throw Error('没有待发布的整站模板');if(s.conflict)throw Error('网站已经变化,请重新预检并暂存');if(!confirm('将由 Owner 正式应用整站布局,并保留原网站业务内容与 SEO。继续?'))return;show('templateResult',await api(`/api/factory/sites/${current()}/whole-site-templates/staged/publish`,'POST',{expected_revision:s.revision}))}catch(e){show('templateResult','错误:'+e.message)}};
$('build').onclick=async()=>{try{show('buildResult',await api(`/api/sites/${current()}/builds`,'POST',{mirror_images:false}))}catch(e){show('buildResult','错误:'+e.message)}};
async function loadWpSessions(){try{if(!current())return;const result=await api(`/api/migrations/sites/${current()}/wordpress/sessions`);const select=$('wpSession');const chosen=select.value;select.replaceChildren(...result.items.map(s=>opt(s.id,`${s.origin} · ${s.state} · ${s.fetched} 条`)));if(result.items.some(s=>s.id===chosen))select.value=chosen;if(select.value){const item=await api(`/api/migrations/sites/${current()}/wordpress/sessions/${select.value}`);show('wpResult',item)}}catch(e){show('wpResult','查询错误:'+e.message)}}
async function wpAction(action){const sid=$('wpSession').value;if(!sid)throw Error('请先创建迁移会话');return api(`/api/migrations/sites/${current()}/wordpress/sessions/${sid}/${action}`,'POST',{})}
$('wpCreate').onclick=async()=>{try{const data=await api(`/api/migrations/sites/${current()}/wordpress/sessions`,'POST',{origin:$('wpOrigin').value,max_items:Number($('wpLimit').value)});await loadWpSessions();$('wpSession').value=data.id;show('wpResult',data)}catch(e){show('wpResult','错误:'+e.message)}};
let wpAutoRunning=false;
$('wpAuto').onclick=async()=>{if(wpAutoRunning)return;wpAutoRunning=true;let count=0;try{while(wpAutoRunning&&count<200){const r=await wpAction('advance');show('wpResult',r);count++;if(['ready','completed','cancelled','limit_reached'].includes(r.state))break;await new Promise(done=>setTimeout(done,350))}await loadWpSessions()}catch(e){show('wpResult','暂停:'+e.message)}finally{wpAutoRunning=false}};
$('wpStop').onclick=()=>{wpAutoRunning=false};
$('wpWorker').onclick=async()=>{try{const sid=$('wpSession').value;show('wpResult',await api(`/api/migrations/sites/${current()}/wordpress/sessions/${sid}/scan`,'POST',{}));await loadWpSessions()}catch(e){show('wpResult','提交失败:'+e.message)}};
$('wpSitemap').onclick=async()=>{try{const sid=$('wpSession').value;show('wpResult',await api(`/api/migrations/sites/${current()}/wordpress/sessions/${sid}/sitemap`,'POST',{}));}catch(e){show('wpResult','Sitemap 失败:'+e.message)}};
$('wpMedia').onclick=async()=>{try{const sid=$('wpSession').value;show('wpResult',await api(`/api/migrations/sites/${current()}/wordpress/sessions/${sid}/media/localize`,'POST',{limit:20}));}catch(e){show('wpResult','媒体迁移失败:'+e.message)}};
$('wpCatalogSync').onclick=async()=>{try{const sid=$('wpSession').value;const resource=$('wpCatalogType').value;show('wpResult',await api(`/api/migrations/sites/${current()}/wordpress/sessions/${sid}/catalog/${resource}/next`,'POST',{}));}catch(e){show('wpResult','目录同步失败:'+e.message)}};
$('wpCatalogStatus').onclick=async()=>{try{show('wpResult',await api(`/api/migrations/sites/${current()}/wordpress/sessions/${$('wpSession').value}/catalog`));}catch(e){show('wpResult','读取目录失败:'+e.message)}};
$('wpNext').onclick=async()=>{try{show('wpResult',await wpAction('advance'));await loadWpSessions()}catch(e){show('wpResult','错误:'+e.message)}};
$('wpFinalize').onclick=async()=>{if(!confirm('采集完成后,确认将全部暂存内容正式导入并登记原 URL/SEO 基线?'))return;try{show('wpResult',await wpAction('finalize'));await loadWpSessions();await loadBatches()}catch(e){show('wpResult','错误:'+e.message)}};
$('wpCancel').onclick=async()=>{if(!confirm('取消此迁移会话?已经暂存的批次不会自动删除。'))return;try{show('wpResult',await wpAction('cancel'));await loadWpSessions()}catch(e){show('wpResult','错误:'+e.message)}};
$('wpReload').onclick=loadWpSessions;
$('wpAudit').onclick=async()=>{try{const builds=await api(`/api/sites/${current()}/builds`);const latest=builds.find(b=>b.status==='complete'||b.status==='done'||b.status==='success'||b.status==='completed');if(!latest)throw Error('尚未找到已完成的静态构建版本');const result=await api(`/api/audit/sites/${current()}/builds/${latest.id}/report`);show('wpResult',{build_id:latest.id,...result})}catch(e){show('wpResult','SEO 验收错误:'+e.message)}};
$('wpSession').onchange=loadWpSessions;
async function loadApiSources(){if(!current())return;try{const res=await api(`/api/connectors/sites/${current()}/api-sources`);const el=$('apiSource'),was=el.value;el.replaceChildren(...res.items.map(s=>opt(s.id,`${s.url} · ${s.state} · ${s.staged_count} 条`)));if(res.items.some(x=>x.id===was))el.value=was;}catch(e){show('p3Result','查询 API 数据源失败:'+e.message)}}
$('xmlStage').onclick=async()=>{try{if(!$('xmlFile').files[0])throw Error('请选择 XML 文件');const form=new FormData();form.append('file',$('xmlFile').files[0]);form.append('record_tag',$('xmlRecord').value);form.append('content_type',$('kind').value);form.append('mapping_json',$('xmlMapping').value);show('p3Result',await api(`/api/connectors/sites/${current()}/xml/staging`,'POST',form));await loadBatches()}catch(e){show('p3Result','XML 导入失败:'+e.message)}};
$('apiCreate').onclick=async()=>{try{const result=await api(`/api/connectors/sites/${current()}/api-sources`,'POST',{url:$('apiUrl').value,items_path:$('apiItems').value,content_type:$('kind').value,page_size:Number($('apiPageSize').value),max_pages:Number($('apiMaxPages').value),mapping:JSON.parse($('apiMapping').value||'{}'),auth_type:$('apiAuth').value,secret_ref:$('apiAuth').value==='none'?'':$('apiSecretRef').value,auth_header:$('apiAuthHeader').value});await loadApiSources();$('apiSource').value=result.id;show('p3Result',result)}catch(e){show('p3Result','创建失败:'+e.message)}};
$('apiReload').onclick=loadApiSources;
$('apiRun').onclick=async()=>{try{if(!$('apiSource').value)throw Error('请先选择数据源');show('p3Result',await api(`/api/connectors/sites/${current()}/api-sources/${$('apiSource').value}/scan`,'POST',{}));await loadApiSources()}catch(e){show('p3Result','提交失败:'+e.message)}};
$('apiStatus').onclick=async()=>{try{show('p3Result',await api(`/api/connectors/sites/${current()}/api-sources/${$('apiSource').value}`));await loadBatches()}catch(e){show('p3Result','查询失败:'+e.message)}};
$('apiCancel').onclick=async()=>{if(!confirm('取消当前 API 数据源的后台采集?已暂存的批次不自动删除。'))return;try{show('p3Result',await api(`/api/connectors/sites/${current()}/api-sources/${$('apiSource').value}/cancel`,'POST',{}));await loadApiSources()}catch(e){show('p3Result','取消失败:'+e.message)}};
async function loadMySources(){if(!current())return;try{const data=await api(`/api/connectors/sites/${current()}/mysql-sources`);const el=$('mySource'),was=el.value;el.replaceChildren(...data.items.map(s=>opt(s.id,`${s.db_name}.${s.table_name} · ${s.state} · ${s.staged_count} 条`)));if(data.items.some(s=>s.id===was))el.value=was}catch(e){show('myResult','读取 MySQL 数据源失败:'+e.message)}}
$('myCreate').onclick=async()=>{try{const result=await api(`/api/connectors/sites/${current()}/mysql-sources`,'POST',{host:$('myHost').value,database:$('myDatabase').value,table:$('myTable').value,key_column:$('myKey').value,columns:$('myColumns').value.split(',').map(s=>s.trim()).filter(Boolean),user_ref:$('myUserRef').value,password_ref:$('myPassRef').value,page_size:Number($('myPageSize').value),max_pages:Number($('myMaxPages').value),content_type:$('kind').value,mapping:JSON.parse($('myMapping').value||'{}')});await loadMySources();$('mySource').value=result.id;show('myResult',result)}catch(e){show('myResult','创建失败:'+e.message)}};
$('myReload').onclick=loadMySources;
$('myRun').onclick=async()=>{try{show('myResult',await api(`/api/connectors/sites/${current()}/mysql-sources/${$('mySource').value}/scan`,'POST',{}))}catch(e){show('myResult','提交失败:'+e.message)}};
$('myStatus').onclick=async()=>{try{show('myResult',await api(`/api/connectors/sites/${current()}/mysql-sources/${$('mySource').value}`));await loadBatches()}catch(e){show('myResult','查询失败:'+e.message)}};
$('myCancel').onclick=async()=>{if(!confirm('取消 MySQL 后台采集?已暂存的批次保留。'))return;try{show('myResult',await api(`/api/connectors/sites/${current()}/mysql-sources/${$('mySource').value}/cancel`,'POST',{}));await loadMySources()}catch(e){show('myResult','取消失败:'+e.message)}};
load();
</script></body></html>
+15
View File
@@ -0,0 +1,15 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SiteForge · 完整主题预览中心</title>
<style>*{box-sizing:border-box}body{margin:0;background:#eef1f5;font:14px/1.6 system-ui,'Microsoft YaHei',sans-serif;color:#172c42}header{display:flex;align-items:center;justify-content:space-between;padding:14px 26px;background:#fff;border-bottom:1px solid #dce3eb;gap:24px;flex-wrap:wrap}header strong{font-size:17px}header strong span{background:#15253e;color:#fff;border-radius:6px;padding:6px 8px;margin-right:8px}header a{color:#255b99;text-decoration:none}.controls{display:flex;align-items:end;flex-wrap:wrap;gap:12px;padding:18px 26px;border-bottom:1px solid #dce3eb;background:white}.controls label{font-weight:700;color:#5c7083;font-size:11px;display:grid;gap:6px}.controls select{border:1px solid #ccd5df;background:#fff;color:#192d41;padding:10px 14px;border-radius:7px;min-width:132px;font-size:13px}.devices{display:flex;gap:4px;margin-left:auto;background:#f0f3f8;border-radius:8px;padding:4px}.devices button{border:0;background:none;padding:9px 14px;border-radius:6px;cursor:pointer;color:#52697e}.devices button.active{background:#1e446c;color:white}.stage{padding:22px 18px 80px;display:flex;justify-content:center;overflow:auto}.frame{width:100%;height:calc(100vh - 210px);min-height:520px;background:white;box-shadow:0 16px 50px #12304720;border-radius:10px;overflow:hidden;transition:width .25s}.frame iframe{border:0;width:100%;height:100%;background:white}.help{text-align:center;color:#6d8296;font-size:12px;margin:-55px auto 28px}.error{color:#c14f45;display:none}@media(max-width:700px){header,.controls{padding:12px}header strong{font-size:14px}.devices{margin-left:0}.frame{min-height:450px}}</style></head>
<body><header><strong><span>SF</span> SiteForge · 主题预览中心</strong><a href="/factory-studio">返回网站工厂 →</a></header>
<section class="controls"><label>主题<select id="theme"><option value="automotive">Automotive · 汽车与出行</option><option value="industrial">Industrial · 工业制造</option><option value="corporate">Corporate · 企业品牌</option><option value="minimal">Minimal · 极简设计</option></select></label>
<label>页面类型<select id="page"><option value="home">首页</option><option value="about">关于我们</option><option value="listing">产品 / 分类列表</option><option value="category">分类详情</option><option value="product">产品详情</option><option value="brand">品牌详情</option><option value="article">新闻详情</option><option value="case">案例详情</option><option value="service">服务详情</option><option value="contact">联系我们</option><option value="faq">常见问题</option><option value="standard">标准页面</option><option value="notfound">404</option></select></label>
<label>主题版本<select id="version"><option value="">最新</option></select></label><div class="devices"><button data-size="100%" class="active">PC</button><button data-size="820px">平板</button><button data-size="390px">手机</button></div><span class="error" id="error">预览失败,请确认已登录。</span></section>
<div class="stage"><div class="frame" id="frame"><iframe id="preview" title="主题真实页面预览" loading="eager"></iframe></div></div>
<div class="help">预览使用真实页面模板与演示数据。实际网站构建后将使用你的品牌信息和内容。</div>
<script>
const theme=document.querySelector('#theme'),page=document.querySelector('#page'),version=document.querySelector('#version'),frame=document.querySelector('#frame'),iframe=document.querySelector('#preview');
function show(){iframe.src=`/api/factory/themes/${encodeURIComponent(theme.value)}/preview?page_type=${encodeURIComponent(page.value)}&version=${encodeURIComponent(version.value)}`;}
async function loadVersions(){const response=await fetch(`/api/factory/themes/${encodeURIComponent(theme.value)}/versions`,{credentials:'same-origin'});if(!response.ok)throw Error('无法读取主题版本');const arr=await response.json();version.replaceChildren();arr.forEach(x=>{const op=new Option('v'+x.version,x.version);version.append(op);});show();}
theme.addEventListener('change',()=>loadVersions().catch(()=>document.querySelector('#error').style.display='inline'));
[page,version].forEach(x=>x.addEventListener('change',show));document.querySelectorAll('.devices button').forEach(x=>x.addEventListener('click',()=>{document.querySelectorAll('.devices button').forEach(y=>y.classList.remove('active'));x.classList.add('active');frame.style.width=x.dataset.size;}));loadVersions().catch(()=>document.querySelector('#error').style.display='inline');
</script></body></html>
+43
View File
@@ -0,0 +1,43 @@
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>SiteForge · 网站工作台</title><meta name="description" content="统一管理网站、内容、设计、迁移和构建。">
<link rel="icon" href="/favicon.ico"><link rel="stylesheet" href="/ui/workspace.css"><script defer src="/ui/workspace.js"></script></head>
<body>
<div class="app-shell">
<aside class="nav-side" id="sideMenu">
<a href="/" class="brand" aria-label="SiteForge 工作台"><span class="brand-icon">S</span><span><strong>SiteForge</strong><small>网站生产工厂</small></span></a>
<div class="nav-group"><div class="nav-label">工作空间</div>
<button data-view="home" class="nav-item active"><span class="nav-ico">▦</span><span>工作台</span></button>
<button data-view="sites" class="nav-item"><span class="nav-ico">▤</span><span>我的网站</span></button>
</div>
<div class="nav-group" id="websiteGroup"><div class="nav-label">当前网站</div><div id="sideSiteName" class="site-mini">请先选择网站</div>
<button data-view="overview" class="nav-item"><span class="nav-ico">◎</span><span>网站概览</span></button>
<button data-view="content" class="nav-item"><span class="nav-ico">▧</span><span>内容与页面</span></button>
<button data-view="design" class="nav-item"><span class="nav-ico">◈</span><span>外观与设计</span></button>
<button data-view="import" class="nav-item"><span class="nav-ico">⇩</span><span>导入与迁移</span></button>
<button data-view="build" class="nav-item"><span class="nav-ico">↗</span><span>构建与交付</span></button>
<button data-view="settings" class="nav-item"><span class="nav-ico">⚙</span><span>网站设置</span></button>
</div>
<details class="advanced"><summary><span class="nav-ico">⌘</span>高级工具 <span class="chevron">⌄</span></summary>
<a href="/production-v8" class="nav-sub">数据导入与模板管理</a>
<a href="/operations" class="nav-sub">迁移复核与发布审批</a>
<a href="/theme-gallery" class="nav-sub">完整主题预览</a>
<a href="/factory-studio" class="nav-sub">高级网站设计</a>
<a href="/classic" class="nav-sub">旧版管理功能</a>
</details>
<div class="side-foot"><span class="side-indicator"></span><span><strong>系统运行中</strong><small id="appVersion">读取版本…</small></span></div>
</aside>
<div class="content-shell">
<header class="topbar">
<div class="top-left"><button class="hamburger" id="menuToggle" aria-label="展开菜单" aria-expanded="false">☰</button><div class="breadcrumbs"><span>工作空间</span><span class="crumb-divider">/</span><strong id="breadcrumbTitle">工作台</strong></div></div>
<div class="top-right"><label class="site-picker-wrap"><span>正在管理</span><select id="sitePicker" aria-label="选择当前网站"><option value="">请选择网站</option></select></label><button id="newSiteTop" class="btn btn-primary">+ 创建网站</button><button class="logout" id="logoutBtn" title="退出登录" aria-label="退出登录">↪</button></div>
</header>
<main class="main-content" id="mainContent" tabindex="-1"><div class="skeleton">正在读取工作空间…</div></main>
<footer class="app-footer"><span>SiteForge · 让网站制作有清晰的下一步</span><span>正式发布前请完成内容与 SEO 检查</span></footer>
</div>
</div>
<div class="mobile-backdrop" id="mobileBackdrop"></div><div id="toast" class="toast" role="status" aria-live="polite"></div>
<dialog id="siteModal" class="modal"><form id="siteForm" class="modal-inner"><div class="modal-top"><div><p class="overline">CREATE A WEBSITE</p><h2>创建新网站</h2><p class="subtle">只需填写基本信息,其他内容都可以稍后完善。</p></div><button type="button" data-close="siteModal" class="x-btn" aria-label="关闭">×</button></div>
<div class="form-fields"><label class="full">网站名称 <input name="name" required maxlength="150" placeholder="例如:HESU Auto 全球官网"></label><label class="full">这次要做什么? <select name="mode"><option value="new">搭建一个全新网站</option><option value="migration">把旧网站迁移过来</option><option value="hybrid">旧内容 + 新内容混合建站</option></select></label><label class="full">选择行业风格 <select name="theme"><option value="automotive">汽车与出行</option><option value="industrial">工业与制造</option><option value="corporate" selected>企业品牌</option><option value="minimal">极简展示</option></select></label><label class="full">网站域名 <span class="optional">选填</span><input name="domain" placeholder="https://example.com"></label><label class="full">一句话介绍 <span class="optional">选填</span><input name="tagline" placeholder="例如:全球汽车出口与服务"></label></div><div class="modal-actions"><button type="button" data-close="siteModal" class="btn btn-muted">取消</button><button type="submit" class="btn btn-primary">创建并开始 →</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>
</body></html>