diff --git a/README_V8_34.md b/README_V8_34.md
new file mode 100644
index 0000000..184bca2
--- /dev/null
+++ b/README_V8_34.md
@@ -0,0 +1,36 @@
+# SiteForge Enterprise V8.34.0-dev.1 — 五主题可视化布局优化(第一批)
+
+**基线**:V8.33.1(HESU PIPE 首页布局修复);**主路线**:用户上传的 `COMPLETION_PLAN.md`,V8.34 主题商业化与易用编辑器阶段。
+
+## 交付范围(本轮实际落地)
+
+- **Banner 直接改布局**:选中封面横幅,在右侧内容设置以可点击卡片选择「文字左 / 图片左 / 标题居中」,不需要设置 CSS、JSON 或行列参数。
+- **图片显示方式**:图片铺满或完整显示,适用于车型外观、HDPE 管材等不应被裁切的商品图;仍通过本站素材库上传、选择图片。
+- **动态内容列表**:选择标准、紧凑、图片优先的卡片形式;后台仍绑定真实车型/产品数据,不复制产品信息,页面 URL 和 SEO 不变。
+- **一致的存储和渲染**:新字段 `hero.layout_variant`、`hero.image_fit`、`collection.card_style`、`collection.image_fit` 经过 `validate_blocks` 白名单验证;正式渲染与编辑器预览共用 `render_blocks` 和 `app/static/blocks.css`。
+- **旧站保护**:老区块若没有新字段,则不生成新布局类名,旧表现保持;原五套主题版本与 1.3.1 工业主题均不修改。只有主动选用新布局才改变页面。
+- **移动端与性能**:响应式排序、减少无意义箭头、新 Banner 首页图片 `loading=eager` 与 `fetchpriority=high`;详情/内页依旧按需加载。
+
+## 操作路径
+
+进入后台 → 网站 → 页面 → 可视化编辑 → 点选 Banner 或动态内容列表 → 右侧 **内容设置** → 点击布局选项 → 中间实时预览 → 保存草稿 → 检查并发布设计 → 构建部署。
+
+点击「从素材库选择 / 上传新图片」即可更换当前图片区块素材。其它高级样式设置仍在单独的「更多设计设置」中。
+
+## 质量验收
+
+- 19/19 批独立 Python 回归通过,合计 **448 个测试**(详见 `reports/v834-regression/result.json`及原始日志)。
+- 39/39 JavaScript 测试通过(`node --test tests_js/*.cjs`)。
+- HTTP + Worker 登录、CSRF、创建网站、构建、ZIP 下载、静态预览通过(`scripts/http_end_to_end.py`)。
+- **36 组** Chromium 独立页面状态检查:汽车/工业 × 三种 Banner × 三种卡片 × 电脑/390px,浏览器脚本错误 0、横向溢出 0。
+- `scripts/verify_release.py` 与 `node --check app/static/editor.js` 通过。
+
+### 演示与报告
+
+- `examples/V8_34_主题Banner与产品卡片_真实渲染交互预览.html`:服务端实际渲染 HTML 的离线交互对比,不连接正式站点。
+- `reports/v834/V8_34_可视化布局阶段验收.html`、`reports/v834/browser-check.json`,含所有测试记录与生产限制。
+- `scripts/v834_visual_preview.py`、`scripts/v834_browser_check.py` 可复现示例与浏览器检查。
+
+## 本轮没有宣称完成的内容
+
+五套主题的 14 类页面逐页人工商业视觉验收,车型参数表格/全局导航的画布级所见即所得编辑,真实企业图片素材规范化,Art Design Pro 官方工程编译,PG/Redis 多 Worker 生产压力和公网部署/回滚验证。下阶段继续优先完善这些操作,但必须先测试当前新增字段在真实网站草稿上的表现。
diff --git a/README_V8_34_1.md b/README_V8_34_1.md
new file mode 100644
index 0000000..cc48201
--- /dev/null
+++ b/README_V8_34_1.md
@@ -0,0 +1,49 @@
+# SiteForge Enterprise V8.34.1-dev.1 — 多图 Banner 与参数表可视化编辑
+
+本版本以已验收的 **V8.34.0-dev.1** 为基线,执行融合开发计划中 P4(主题/可视化编辑)的下一阶段,不替代原项目,不直接修改线上网站。
+
+## 给非技术用户的操作说明
+
+### 首页轮播横幅
+
+1. 打开站点的**可视化编辑器**,选择首页的“封面横幅”。
+2. 在右侧 **多图 Banner(可选)** 点击 **添加一张横幅**。第一次会复制当前横幅内容,因此原图与文字不会丢失。
+3. 在每张卡片里编辑标题、副标题、说明、按钮文字及链接。图片可直接上传或从当前站点素材库选择。
+4. 使用 **上移 / 下移 / 删除** 调整图片顺序。最多六张。删除全部后恢复之前的单图 Banner。
+5. 保存页面草稿,点击预览检查电脑和手机,完成原有审核、构建、部署流程。
+
+轮播不自动播放,手机可左右滑动,电脑可点击上一张/下一张;首张首页图片具有优先加载标记,其余图片延迟加载。为了保持标题语义,轮播只有第一张使用 `H1`,之后为 `H2`。
+
+### 产品参数表
+
+1. 选择页面中的“参数信息表”。
+2. 在“参数内容来源”选择 **从产品数据自动读取 / 只显示手动填写 / 产品数据 + 手动补充**。
+3. 如果选择后两项,点击“添加一项”,填写 **参数名称** 和 **数值**。无需 JSON。
+4. 保存草稿并预览。默认模式继续使用内容数据,不改写原产品字段。
+
+注意:手动添加的参数属于**当前页面的布局草稿**,不会写回产品数据表;如果多个详情页使用相同布局模板,应检查模板复用和页面数据的适配性。
+
+## 主要源码变动
+
+- `app/blueprints.py`:新增 Banner slides 和参数表 source_mode/items 的声明、严格校验与静态渲染;旧区块不受影响。
+- `app/static/editor.js`:复用原有数组编辑器,上移、下移、复制首张、上传与素材库定点替换、参数来源选择。
+- `app/static/site-runtime.js`:无外部依赖、无自动轮播定时器、原生滑动及键盘可达的手动控制。
+- `app/static/blocks.css`、`editor.css`:响应式 Banner、可见焦点和紧凑输入。
+- `tests/test_v8341_slide_specs.py`:参数边界、XSS、SEO、兼容与真实构建测试。
+- `examples/V8_34_1_多图Banner与参数表_真实渲染预览.html`:原项目渲染函数和原始静态 CSS/JS 生成的独立演示。
+
+## 不回退的要求
+
+五套主题,旧主题锁定版本,主题预览安全限制,编辑器简洁模式、嵌套组件、历史记录、URL/SEO 保留,发布审批与 Worker 流程均保留。平台是开发集成版;尚未完成的 Art Design Pro 官方源工程集成、真实 PostgreSQL/Redis 多 Worker、生产远程部署及正式登录后台全浏览器 E2E 不得标记为完成。
+
+## 升级及回滚
+
+1. 备份原 V8.34 源码和站点数据;**不要直接覆盖线上正在运行的服务**。
+2. 在独立环境解压本完整源码,使用 `.env.example` 建立独立配置。不可从交付包中寻找正式密钥。
+3. `python scripts/verify_release.py`;`python -m pytest -q tests/test_v8341_slide_specs.py`;`node --test tests_js/*.cjs tests/test_nested_helpers_v823.cjs`。
+4. 在测试站点新建 Banner 多图和参数内容、预览、构建并比对。确认历史主题版本没有改变后再走原审批流程上线。
+5. 回滚时使用前一版源码和经过备份的数据,重新按原构建/发布流程执行。已有新字段在回滚前需完成兼容性核对。
+
+## 下一阶段范围
+
+继续按融合计划推进更直观的栏目布局、图片裁切与焦点设置、嵌套轮播图管理、完整五主题跨页面视觉测试;随后进入 V8.35 的 WordPress 迁移数据完整性专项。
diff --git a/app/blueprints.py b/app/blueprints.py
index 7bd9630..fd668c2 100644
--- a/app/blueprints.py
+++ b/app/blueprints.py
@@ -9,16 +9,16 @@ from urllib.parse import urlparse
from .core import sanitize, route_path
BLOCKS = {
- 'hero': {'name':'封面横幅','icon':'✦','fields':['title','subtitle','description','image','button','href','align'], 'defaults':{'title':'让品牌从这里出发','subtitle':'GLOBAL EXPERIENCE','description':'在这里呈现企业最重要的价值。','button':'探索产品','href':'/product/','align':'left'}},
+ 'hero': {'name':'封面横幅','icon':'✦','fields':['title','subtitle','description','image','button','href','align','layout_variant','image_fit','slides'], 'defaults':{'title':'让品牌从这里出发','subtitle':'GLOBAL EXPERIENCE','description':'在这里呈现企业最重要的价值。','button':'探索产品','href':'/product/','align':'left','layout_variant':'split','image_fit':'cover','slides':[]}},
'richtext':{'name':'富文本','icon':'≡','fields':['title','body'],'defaults':{'title':'关于我们','body':'
`;
- for(const field of spec.fields.filter(f=>b.type!=='layout'||!['columns','gap','tablet_columns','mobile_columns'].includes(f))){let value=b[field]??'',label=({title:'标题',subtitle:'副标题',description:'简介',image:'图片地址',body:'正文 HTML',button:'按钮文字',href:'跳转路径',source:'绑定内容类型',limit:'显示条数',columns:'每行列数',items:'重复条目',images:'画廊图片',image_side:'图片方向',align:'标题对齐',height:'间隔高度'})[field]||field;
+ for(const field of spec.fields.filter(f=>(b.type!=='layout'||!['columns','gap','tablet_columns','mobile_columns'].includes(f))&&!(b.type==='hero'&&b.slides?.length&&['title','subtitle','description','image','button','href'].includes(f))&&!(b.type==='attributes'&&(b.source_mode||'page')==='page'&&f==='items'))){let value=b[field]??'',label=({title:'标题',subtitle:'副标题',description:'简介',image:'图片地址',body:'正文 HTML',button:'按钮文字',href:'跳转路径',source:'绑定内容类型',limit:'显示条数',columns:'每行列数',items:'重复条目',images:'画廊图片',image_side:'图片方向',align:'标题对齐',height:'间隔高度',layout_variant:'Banner 排版',card_style:'卡片展示方式',image_fit:'图片显示方式',slides:'多图 Banner(可选)',source_mode:'参数内容来源'})[field]||field;
out+=`
${html(label)} `;
- if(field==='items'||field==='images')out+=Studio.arrayEditor(field,value,b.type);
+ if(field==='items'||field==='images'||field==='slides')out+=(field==='slides'?'
首次添加会复制当前 Banner;之后直接修改每张图的文字、按钮和图片。最多 6 张,访客手动切换。删除全部幻灯片后恢复原来的单图 Banner。
':field==='items'&&b.type==='attributes'?'
可以手动添加参数,不需要填写 JSON;仅在选择“手动参数”或“数据 + 补充”时显示。
':'')+Studio.arrayEditor(field,value,b.type);
+ else if(field==='source_mode')out+=`
${[['page','从产品数据自动读取'],['custom','只显示手动填写'],['merge','产品数据 + 手动补充']].map(([v,n])=>`${n} `).join('')} 默认读取产品数据,不会更改原产品信息。 `;
else if(field==='source')out+=`
${['product','post','page','vehicle','brand','case','service','other','all'].map(t=>`${({product:'产品',post:'文章',page:'普通页面',vehicle:'车型',brand:'品牌',case:'案例',service:'服务',other:'其他',all:'全部类型'})[t]} `).join('')} 网站已有内容将自动绑定并生成卡片。 `;
+ else if(['layout_variant','card_style','image_fit'].includes(field)){
+ const choices={layout_variant:[['split','文字左侧 · 图片右侧'],['image-first','图片左侧 · 文字右侧'],['centered','标题居中 · 图片下方']],card_style:[['standard','标准卡片'],['compact','紧凑型 · 展示更多产品'],['showcase','图片优先 · 大图展示']],image_fit:[['cover','铺满画面(可能裁切)'],['contain','完整显示(不裁切)']]}[field];
+ out+=`
${choices.map(([v,name])=>`${v==='split'?'▤':v==='image-first'?'▥':v==='centered'?'▦':v==='compact'?'▤':v==='showcase'?'▧':v==='contain'?'▣':'▦'} ${html(name)} `).join('')}
`;
+ if(field==='image_fit')out+='
汽车与管材产品建议选择“完整显示”,避免产品轮廓被裁切。 ';
+ }
else if(field==='image_side'||field==='align')out+=`
${(field==='align'?['left','center']:['left','right']).map(v=>`${v==='left'?'左侧':v==='right'?'右侧':'居中'} `).join('')} `;
else if(field==='body'||field==='description')out+=`
`;
else if(['limit','columns','height'].includes(field))out+=`
`;
@@ -383,7 +389,7 @@ const Studio={
}
const mediaTarget=selectedChildId?editorTools.findChildInRoot(blocks,b.id,selectedChildId)?.child:b;
const mediaFields=schema.blocks[mediaTarget?.type]?.fields||[];
- if(mediaTarget&&(['gallery','carousel'].includes(mediaTarget.type)||mediaFields.includes('image')))out+=`
图片设置 从图片素材库选择 或上传一张新图片 预览先显示在草稿里,点击保存后才会保留。
`;
+ if(mediaTarget&&!(mediaTarget.type==='hero'&&mediaTarget.slides?.length)&&(['gallery','carousel'].includes(mediaTarget.type)||mediaFields.includes('image')))out+=`
图片设置 从图片素材库选择 或上传一张新图片 预览先显示在草稿里,点击保存后才会保留。
`;
if(b.type==='layout')out+=Studio.childQuickEditor(b);
out+='
';
if(b.type==='layout'){
@@ -619,20 +625,24 @@ const Studio={
removeChild(i,j){const b=blocks[selected];if(!b||b.type!=='layout')return;b.columns[i].blocks.splice(j,1);Studio.modified();Studio.inspector()},
childField(i,j,key,value){const b=blocks[selected];if(!b||b.type!=='layout')return;b.columns[i].blocks[j][key]=value;Studio.modified()},
moveChild(ci,ri,target){const b=blocks[selected];if(!b||b.type!=='layout'||target<0||target>=b.columns.length)return;let [component]=b.columns[ci].blocks.splice(ri,1);b.columns[target].blocks.push(component);Studio.modified();Studio.inspector()},
- async chooseMedia(){try{const data=await api(`/api/sites/${siteId}/media`);const grid=$('#siteMediaGrid');grid.replaceChildren();if(!data.length){grid.textContent='暂无上传图片。可先在当前组件中上传图片。'}for(const item of data){const button=document.createElement('button');button.type='button';button.style.cssText='border:1px solid #e2e8f0;border-radius:9px;background:white;padding:7px;cursor:pointer';const img=document.createElement('img');img.alt=item.name;img.loading='lazy';img.style.cssText='width:100%;height:105px;object-fit:contain';img.src=`/local-media/${encodeURIComponent(siteId)}/${encodeURIComponent(item.name)}`;const label=document.createElement('small');label.textContent=item.name;label.style.cssText='display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';button.append(img,label);button.addEventListener('click',()=>Studio.applyMedia(item.url));grid.append(button)}$('#siteMediaModal').showModal()}catch(e){toast(e.message,true)}},
- applyMedia(url){
+ async chooseMedia(field=null,index=null){try{const data=await api(`/api/sites/${siteId}/media`);const grid=$('#siteMediaGrid');grid.replaceChildren();if(!data.length){grid.textContent='暂无上传图片。可先在当前组件中上传图片。'}for(const item of data){const button=document.createElement('button');button.type='button';button.style.cssText='border:1px solid #e2e8f0;border-radius:9px;background:white;padding:7px;cursor:pointer';const img=document.createElement('img');img.alt=item.name;img.loading='lazy';img.style.cssText='width:100%;height:105px;object-fit:contain';img.src=`/local-media/${encodeURIComponent(siteId)}/${encodeURIComponent(item.name)}`;const label=document.createElement('small');label.textContent=item.name;label.style.cssText='display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';button.append(img,label);button.addEventListener('click',()=>Studio.applyMedia(item.url,field,index));grid.append(button)}$('#siteMediaModal').showModal()}catch(e){toast(e.message,true)}},
+ applyMedia(url,field=null,index=null){
const root=blocks[selected];if(!root)return;
const found=selectedChildId?editorTools.findChildInRoot(blocks,root.id,selectedChildId):null;
if(selectedChildId&&!found)return toast('当前子组件已失效,请重新选中后添加图片',true);
const block=found?.child||root;
const fields=schema?.blocks?.[block.type]?.fields||[];
- if(block.type==='gallery')block.images=[...(block.images||[]),{url,alt:''}];
+ if(field==='slides'){
+ if(block.type!=='hero'||!Number.isInteger(index)||index<0||index>=(block.slides||[]).length)return toast('所选横幅不存在,请重新选择',true);
+ block.slides[index].image=url;
+ }
+ else if(block.type==='gallery')block.images=[...(block.images||[]),{url,alt:''}];
else if(block.type==='carousel')block.items=[...(block.items||[]),{image:url,alt:'',caption:''}];
else if(fields.includes('image'))block.image=url;
else return toast('选中的组件不支持图片,请先选择一个图片组件',true);
Studio.modified();Studio.draw();$('#siteMediaModal').close();toast('已应用素材到草稿');
},
- async uploadImage(file){if(!file)return;try{const form=new FormData();form.append('file',file);let r=await api(`/api/sites/${siteId}/media`,{method:'POST',body:form});Studio.applyMedia(r.url);toast('图片已上传,保存页面后参与构建')}catch(e){toast(e.message,true)}},
+ async uploadImage(file,field=null,index=null){if(!file)return;try{const form=new FormData();form.append('file',file);let r=await api(`/api/sites/${siteId}/media`,{method:'POST',body:form});if(field==='slides')Studio.applyMedia(r.url,field,index);else Studio.applyMedia(r.url);toast('图片已上传,保存页面后参与构建')}catch(e){toast(e.message,true)}},
async buildSite(){if(dirty||draftRevision||siteDesignRevision)return toast('有尚未发布的页面或全站草稿,请先发布,再构建网站',true);try{
toast('已提交后台构建任务');const task=await api(`/api/sites/${siteId}/builds`,req('POST',{mirror_images:false}));
for(let i=0;i<240;i++){
diff --git a/app/static/site-runtime.js b/app/static/site-runtime.js
index 1be7c91..9d68a19 100644
--- a/app/static/site-runtime.js
+++ b/app/static/site-runtime.js
@@ -2,6 +2,16 @@
(()=>{
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
const menu=$('.sf-menu-toggle');if(menu){menu.addEventListener('click',()=>{let open=menu.getAttribute('aria-expanded')!=='true';menu.setAttribute('aria-expanded',String(open));$('#sf-primary-links')?.classList.toggle('is-open',open);});}
+ // V8.34.1: accessible, manual carousel. Native scroll/swipe works without JS.
+ $$('.sf-hero-slider[data-sf-hero-slider]').forEach(slider=>{
+ const track=slider.querySelector('.sf-hero-track');const panels=[...slider.querySelectorAll('.sf-hero-panel')];
+ if(!track||panels.length<2)return;
+ const counter=slider.querySelector('.sf-hero-count');let index=0;
+ const reduced=window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
+ function choose(next){index=(next+panels.length)%panels.length;track.scrollTo({left:panels[index].offsetLeft-panels[0].offsetLeft,behavior:reduced?'instant':'smooth'});if(counter)counter.textContent=`${index+1} / ${panels.length}`;}
+ slider.querySelectorAll('[data-sf-slide-step]').forEach(btn=>btn.addEventListener('click',()=>choose(index+Number(btn.dataset.sfSlideStep))));
+ let raf=false;track.addEventListener('scroll',()=>{if(raf)return;raf=true;requestAnimationFrame(()=>{const left=track.scrollLeft;let best=0,delta=Infinity;panels.forEach((panel,i)=>{const d=Math.abs(panel.offsetLeft-panels[0].offsetLeft-left);if(d
{
const cards=$$('.sf-item',grid);
diff --git a/app/version.py b/app/version.py
index d050c45..76ef821 100644
--- a/app/version.py
+++ b/app/version.py
@@ -1,5 +1,5 @@
"""Single source of truth for the SiteForge platform release identity."""
import os
-VERSION = "8.33.1-dev.1"
+VERSION = "8.34.1-dev.1"
PLATFORM_NAME = "SiteForge Factory"
BUILD_SHA = os.environ.get("SITEFORGE_GIT_SHA", "unknown").strip()[:40] or "unknown"
diff --git a/docs/V8_34_1_EDITOR_IMPLEMENTATION.md b/docs/V8_34_1_EDITOR_IMPLEMENTATION.md
new file mode 100644
index 0000000..480e0c0
--- /dev/null
+++ b/docs/V8_34_1_EDITOR_IMPLEMENTATION.md
@@ -0,0 +1,26 @@
+# V8.34.1 实施与验收技术说明
+
+## 变更模型
+
+### hero
+
+- 新增可选 `slides: [{title, subtitle, description, image, button, href}]`;最多六条,字段仅允许字符串,单值最长 2000 字符。
+- `slides=[]` 或字段不存在时,原单图渲染逻辑保持不变。
+- 含幻灯片时每张使用完整的按钮与链接,图片地址仅允许原项目 `safe_media` 规则;使用 `safe_href` 过滤目标 URL;所有文字在 HTML 边界做转义。
+- 首页首张图 loading=eager、fetchpriority=high,后续图片 lazy;从第二张开始使用 `H2`,但保留同一视觉字号。
+- 使用 scroll-snap 和原生水平滚动,JS 仅绑定手动上一张/下一张与当前页数;不添加计时器/轮播依赖;键盘能聚焦滚动区,支持 reduced motion。
+
+### attributes
+
+- 增加 `source_mode`: `page`(默认)、`custom` 或 `merge`;新增 `items:[{label,value}]`,最多 30 项,字符串最长 300 字。
+- `page`: 现有页面绑定产品参数;`custom`: 仅展示手动参数;`merge`: 保留页面参数并追加手动内容。
+- 所有字段均经服务端白名单校验;手动值通过 `resolve_text` 后 HTML 转义,不允许直接注入 HTML。
+- 编辑器在自动数据模式隐藏手动项,降低冗余操作;切换回手动模式时未删除的手动项仍在草稿里。
+
+## 已知边界
+
+- 多图编辑重点面向 **页面顶层 hero 区块**。嵌套布局里的 hero 虽可渲染,其独立 slides 编辑入口仍待下一阶段完善。
+- 当前展示仅为手动轮播,没有自动轮播、媒体压缩和自动热点裁切功能。用户上传的原图大小仍会影响真实速度。
+- 未对五套主题的全部真实页和所有终端做正式视觉基线对比。本轮实测的是原 CSS/JS 生成的独立页面与现有构建测试。
+- 手动参数在页面布局中保存,不回写 CMS,避免影响其他内容与 SEO。
+- 当前未进行生产数据库、完整 Art Design Pro 源工程及正式远程发布验收。
diff --git a/examples/V8_34_1_多图Banner与参数表_真实渲染预览.html b/examples/V8_34_1_多图Banner与参数表_真实渲染预览.html
new file mode 100644
index 0000000..6f30721
--- /dev/null
+++ b/examples/V8_34_1_多图Banner与参数表_真实渲染预览.html
@@ -0,0 +1,115 @@
+SiteForge V8.34.1 · Banner & 产品参数真实渲染预览 SiteForge V8.34.1 · 实际渲染预览 横幅逐张编辑 · 手动切换 · 参数可补充
静态构建同源渲染 · 演示数据 CHINA SOURCING. GLOBAL DELIVERY. 全球汽车采购与交付 车辆采购、出口手续、国际物流到海外交付,一站完成。
了解业务 ✧
AUTOMOTIVE EXPORT 专业车型与可靠服务 为不同市场匹配乘用车、皮卡与新能源车型。
查看车型 ✧
AFTER-SALES SUPPORT 从采购开始的长期合作 本地交付及备件与售后协同,支持长期业务发展。
联系我们 ✧
‹ 1 / 3 ›
车型示例参数 品牌 Sample Brand
类别 Export Vehicles
动力形式 纯电动 / 插电混动
可选配置 按出口市场和车型版本核对
此页面通过原项目 render_blocks() 生成区块,并内联相同的 blocks.css 与 site-runtime.js。可点击 Banner 左右控制按钮或在手机上横向滑动。参数同时显示自动数据与人工补充内容。图片位置为无素材占位,不代表正式网站。
\ No newline at end of file
diff --git a/examples/V8_34_主题Banner与产品卡片_真实渲染交互预览.html b/examples/V8_34_主题Banner与产品卡片_真实渲染交互预览.html
new file mode 100644
index 0000000..574ec3b
--- /dev/null
+++ b/examples/V8_34_主题Banner与产品卡片_真实渲染交互预览.html
@@ -0,0 +1,7 @@
+SiteForge V8.34 · 真正生成的主题布局
+
+
+
\ No newline at end of file
diff --git a/reports/v834-regression-initial/result.json b/reports/v834-regression-initial/result.json
new file mode 100644
index 0000000..9676687
--- /dev/null
+++ b/reports/v834-regression-initial/result.json
@@ -0,0 +1,216 @@
+{
+ "version": "8.34.0-dev.1",
+ "generated_utc": "2026-10-26T05:55:36.031064+00:00",
+ "groups": [
+ {
+ "batch": 1,
+ "files": [
+ "test_commercial_v5.py",
+ "test_connectors_v84.py",
+ "test_connectors_v85.py"
+ ],
+ "status": "passed",
+ "seconds": 5.62,
+ "summary": "................................... [100%]\n35 passed in 3.46s\n\n"
+ },
+ {
+ "batch": 2,
+ "files": [
+ "test_console_v819.py",
+ "test_core_v4.py",
+ "test_deep_nested_v824.py"
+ ],
+ "status": "passed",
+ "seconds": 5.53,
+ "summary": "........................... [100%]\n27 passed in 3.49s\n\n"
+ },
+ {
+ "batch": 3,
+ "files": [
+ "test_editor_easy_v8212.py",
+ "test_editor_ux_v8211.py",
+ "test_enterprise.py"
+ ],
+ "status": "passed",
+ "seconds": 4.05,
+ "summary": "........... [100%]\n11 passed in 1.88s\n\n"
+ },
+ {
+ "batch": 4,
+ "files": [
+ "test_enterprise_v7.py",
+ "test_factory.py",
+ "test_factory_v2.py"
+ ],
+ "status": "passed",
+ "seconds": 7.62,
+ "summary": "................................... [100%]\n35 passed in 5.47s\n\n"
+ },
+ {
+ "batch": 5,
+ "files": [
+ "test_hesupipe_layout_v8331.py",
+ "test_mobile_tabs_v8213.py",
+ "test_multilingual_v815.py"
+ ],
+ "status": "passed",
+ "seconds": 5.95,
+ "summary": "........................ [100%]\n24 passed in 3.88s\n\n"
+ },
+ {
+ "batch": 6,
+ "files": [
+ "test_nested_v823.py",
+ "test_p0_71.py",
+ "test_p0_backup.py"
+ ],
+ "status": "passed",
+ "seconds": 3.11,
+ "summary": "......... [100%]\n9 passed in 0.98s\n\n"
+ },
+ {
+ "batch": 7,
+ "files": [
+ "test_p0_p4_completion_v88.py",
+ "test_p0_p4_completion_v89.py",
+ "test_p1_platform.py"
+ ],
+ "status": "passed",
+ "seconds": 7.41,
+ "summary": "................................ [100%]\n32 passed in 5.17s\n\n"
+ },
+ {
+ "batch": 8,
+ "files": [
+ "test_production_v816.py",
+ "test_professional_ui_v811.py",
+ "test_regression_runner_v817.py"
+ ],
+ "status": "passed",
+ "seconds": 4.43,
+ "summary": "................... [100%]\n19 passed in 2.26s\n\n"
+ },
+ {
+ "batch": 9,
+ "files": [
+ "test_release_native_v814.py",
+ "test_release_reviews_v7.py",
+ "test_release_v813.py"
+ ],
+ "status": "passed",
+ "seconds": 3.8,
+ "summary": "..................... [100%]\n21 passed in 1.54s\n\n"
+ },
+ {
+ "batch": 10,
+ "files": [
+ "test_site_design_v87.py",
+ "test_siteforge_v817_audit.py",
+ "test_studio_v86.py"
+ ],
+ "status": "passed",
+ "seconds": 6.05,
+ "summary": "............................................. [100%]\n45 passed in 3.89s\n\n"
+ },
+ {
+ "batch": 11,
+ "files": [
+ "test_theme_pro_v3.py",
+ "test_theme_registry.py",
+ "test_v818_pg_recovery.py"
+ ],
+ "status": "passed",
+ "seconds": 11.58,
+ "summary": ".......................................... [100%]\n42 passed in 9.25s\n\n"
+ },
+ {
+ "batch": 12,
+ "files": [
+ "test_v818_sqlite_pg_transfer.py",
+ "test_v821_editor_history_and_mapping.py",
+ "test_v822_canvas.py"
+ ],
+ "status": "passed",
+ "seconds": 3.36,
+ "summary": "............ [100%]\n12 passed in 1.28s\n\n"
+ },
+ {
+ "batch": 13,
+ "files": [
+ "test_v825_shared_components.py",
+ "test_v826_global_design.py",
+ "test_v827_guided_workspace.py"
+ ],
+ "status": "passed",
+ "seconds": 4.39,
+ "summary": "................... [100%]\n19 passed in 2.40s\n\n"
+ },
+ {
+ "batch": 14,
+ "files": [
+ "test_v828_launch_center.py",
+ "test_v829_theme_studio.py",
+ "test_v830_real_pages.py"
+ ],
+ "status": "passed",
+ "seconds": 9.91,
+ "summary": "........... [100%]\n11 passed in 7.74s\n\n"
+ },
+ {
+ "batch": 15,
+ "files": [
+ "test_v8311_performance_icons.py",
+ "test_v831_automotive_ref.py",
+ "test_v832_preview_easy_editor.py"
+ ],
+ "status": "passed",
+ "seconds": 5.67,
+ "summary": ".......... [100%]\n10 passed in 3.53s\n\n"
+ },
+ {
+ "batch": 16,
+ "files": [
+ "test_v833_theme_integration.py",
+ "test_v834_visual_presets.py",
+ "test_v8_art_installer.py"
+ ],
+ "status": "passed",
+ "seconds": 3.88,
+ "summary": "................ [100%]\n16 passed in 1.71s\n\n"
+ },
+ {
+ "batch": 17,
+ "files": [
+ "test_v8_workflows.py",
+ "test_wordpress_baseline.py",
+ "test_worker_queue_v6.py"
+ ],
+ "status": "passed",
+ "seconds": 7.01,
+ "summary": "......................... [100%]\n25 passed in 4.94s\n\n"
+ },
+ {
+ "batch": 18,
+ "files": [
+ "test_workspace_v810.py",
+ "test_workspace_v812.py",
+ "test_wp_complex_v820.py"
+ ],
+ "status": "passed",
+ "seconds": 4.05,
+ "summary": "......................... [100%]\n25 passed in 1.88s\n\n"
+ },
+ {
+ "batch": 19,
+ "files": [
+ "test_wp_migration_v81.py",
+ "test_wp_migration_v82.py",
+ "test_wp_migration_v83.py"
+ ],
+ "status": "passed",
+ "seconds": 7.19,
+ "summary": ".............................. [100%]\n30 passed in 4.76s\n\n"
+ }
+ ],
+ "all_passed": true
+}
\ No newline at end of file
diff --git a/reports/v834-regression-prior/result.json b/reports/v834-regression-prior/result.json
new file mode 100644
index 0000000..ced7043
--- /dev/null
+++ b/reports/v834-regression-prior/result.json
@@ -0,0 +1,216 @@
+{
+ "version": "8.34.0-dev.1",
+ "generated_utc": "2026-10-26T06:01:13.067295+00:00",
+ "groups": [
+ {
+ "batch": 1,
+ "files": [
+ "test_commercial_v5.py",
+ "test_connectors_v84.py",
+ "test_connectors_v85.py"
+ ],
+ "status": "passed",
+ "seconds": 6.27,
+ "summary": "................................... [100%]\n35 passed in 3.96s\n\n"
+ },
+ {
+ "batch": 2,
+ "files": [
+ "test_console_v819.py",
+ "test_core_v4.py",
+ "test_deep_nested_v824.py"
+ ],
+ "status": "passed",
+ "seconds": 5.96,
+ "summary": "........................... [100%]\n27 passed in 3.74s\n\n"
+ },
+ {
+ "batch": 3,
+ "files": [
+ "test_editor_easy_v8212.py",
+ "test_editor_ux_v8211.py",
+ "test_enterprise.py"
+ ],
+ "status": "passed",
+ "seconds": 4.39,
+ "summary": "........... [100%]\n11 passed in 2.07s\n\n"
+ },
+ {
+ "batch": 4,
+ "files": [
+ "test_enterprise_v7.py",
+ "test_factory.py",
+ "test_factory_v2.py"
+ ],
+ "status": "passed",
+ "seconds": 7.8,
+ "summary": "................................... [100%]\n35 passed in 5.53s\n\n"
+ },
+ {
+ "batch": 5,
+ "files": [
+ "test_hesupipe_layout_v8331.py",
+ "test_mobile_tabs_v8213.py",
+ "test_multilingual_v815.py"
+ ],
+ "status": "passed",
+ "seconds": 5.58,
+ "summary": "........................ [100%]\n24 passed in 3.58s\n\n"
+ },
+ {
+ "batch": 6,
+ "files": [
+ "test_nested_v823.py",
+ "test_p0_71.py",
+ "test_p0_backup.py"
+ ],
+ "status": "passed",
+ "seconds": 3.01,
+ "summary": "......... [100%]\n9 passed in 0.96s\n\n"
+ },
+ {
+ "batch": 7,
+ "files": [
+ "test_p0_p4_completion_v88.py",
+ "test_p0_p4_completion_v89.py",
+ "test_p1_platform.py"
+ ],
+ "status": "passed",
+ "seconds": 7.27,
+ "summary": "................................ [100%]\n32 passed in 5.16s\n\n"
+ },
+ {
+ "batch": 8,
+ "files": [
+ "test_production_v816.py",
+ "test_professional_ui_v811.py",
+ "test_regression_runner_v817.py"
+ ],
+ "status": "passed",
+ "seconds": 4.31,
+ "summary": "................... [100%]\n19 passed in 2.26s\n\n"
+ },
+ {
+ "batch": 9,
+ "files": [
+ "test_release_native_v814.py",
+ "test_release_reviews_v7.py",
+ "test_release_v813.py"
+ ],
+ "status": "passed",
+ "seconds": 3.38,
+ "summary": "..................... [100%]\n21 passed in 1.32s\n\n"
+ },
+ {
+ "batch": 10,
+ "files": [
+ "test_site_design_v87.py",
+ "test_siteforge_v817_audit.py",
+ "test_studio_v86.py"
+ ],
+ "status": "passed",
+ "seconds": 5.87,
+ "summary": "............................................. [100%]\n45 passed in 3.68s\n\n"
+ },
+ {
+ "batch": 11,
+ "files": [
+ "test_theme_pro_v3.py",
+ "test_theme_registry.py",
+ "test_v818_pg_recovery.py"
+ ],
+ "status": "passed",
+ "seconds": 11.04,
+ "summary": ".......................................... [100%]\n42 passed in 8.76s\n\n"
+ },
+ {
+ "batch": 12,
+ "files": [
+ "test_v818_sqlite_pg_transfer.py",
+ "test_v821_editor_history_and_mapping.py",
+ "test_v822_canvas.py"
+ ],
+ "status": "passed",
+ "seconds": 3.22,
+ "summary": "............ [100%]\n12 passed in 1.19s\n\n"
+ },
+ {
+ "batch": 13,
+ "files": [
+ "test_v825_shared_components.py",
+ "test_v826_global_design.py",
+ "test_v827_guided_workspace.py"
+ ],
+ "status": "passed",
+ "seconds": 4.8,
+ "summary": "................... [100%]\n19 passed in 2.70s\n\n"
+ },
+ {
+ "batch": 14,
+ "files": [
+ "test_v828_launch_center.py",
+ "test_v829_theme_studio.py",
+ "test_v830_real_pages.py"
+ ],
+ "status": "passed",
+ "seconds": 9.62,
+ "summary": "........... [100%]\n11 passed in 7.41s\n\n"
+ },
+ {
+ "batch": 15,
+ "files": [
+ "test_v8311_performance_icons.py",
+ "test_v831_automotive_ref.py",
+ "test_v832_preview_easy_editor.py"
+ ],
+ "status": "passed",
+ "seconds": 5.89,
+ "summary": ".......... [100%]\n10 passed in 3.56s\n\n"
+ },
+ {
+ "batch": 16,
+ "files": [
+ "test_v833_theme_integration.py",
+ "test_v834_visual_presets.py",
+ "test_v8_art_installer.py"
+ ],
+ "status": "passed",
+ "seconds": 3.88,
+ "summary": "................ [100%]\n16 passed in 1.63s\n\n"
+ },
+ {
+ "batch": 17,
+ "files": [
+ "test_v8_workflows.py",
+ "test_wordpress_baseline.py",
+ "test_worker_queue_v6.py"
+ ],
+ "status": "passed",
+ "seconds": 6.94,
+ "summary": "......................... [100%]\n25 passed in 4.75s\n\n"
+ },
+ {
+ "batch": 18,
+ "files": [
+ "test_workspace_v810.py",
+ "test_workspace_v812.py",
+ "test_wp_complex_v820.py"
+ ],
+ "status": "passed",
+ "seconds": 4.01,
+ "summary": "......................... [100%]\n25 passed in 1.76s\n\n"
+ },
+ {
+ "batch": 19,
+ "files": [
+ "test_wp_migration_v81.py",
+ "test_wp_migration_v82.py",
+ "test_wp_migration_v83.py"
+ ],
+ "status": "passed",
+ "seconds": 6.49,
+ "summary": ".............................. [100%]\n30 passed in 4.25s\n\n"
+ }
+ ],
+ "all_passed": true
+}
\ No newline at end of file
diff --git a/reports/v834-regression/result.json b/reports/v834-regression/result.json
new file mode 100644
index 0000000..37e9086
--- /dev/null
+++ b/reports/v834-regression/result.json
@@ -0,0 +1,216 @@
+{
+ "version": "8.34.0-dev.1",
+ "generated_utc": "2026-10-26T06:06:08.349099+00:00",
+ "groups": [
+ {
+ "batch": 1,
+ "files": [
+ "test_commercial_v5.py",
+ "test_connectors_v84.py",
+ "test_connectors_v85.py"
+ ],
+ "status": "passed",
+ "seconds": 5.32,
+ "summary": "................................... [100%]\n35 passed in 3.22s\n\n"
+ },
+ {
+ "batch": 2,
+ "files": [
+ "test_console_v819.py",
+ "test_core_v4.py",
+ "test_deep_nested_v824.py"
+ ],
+ "status": "passed",
+ "seconds": 5.59,
+ "summary": "........................... [100%]\n27 passed in 3.46s\n\n"
+ },
+ {
+ "batch": 3,
+ "files": [
+ "test_editor_easy_v8212.py",
+ "test_editor_ux_v8211.py",
+ "test_enterprise.py"
+ ],
+ "status": "passed",
+ "seconds": 4.05,
+ "summary": "........... [100%]\n11 passed in 1.94s\n\n"
+ },
+ {
+ "batch": 4,
+ "files": [
+ "test_enterprise_v7.py",
+ "test_factory.py",
+ "test_factory_v2.py"
+ ],
+ "status": "passed",
+ "seconds": 7.41,
+ "summary": "................................... [100%]\n35 passed in 5.32s\n\n"
+ },
+ {
+ "batch": 5,
+ "files": [
+ "test_hesupipe_layout_v8331.py",
+ "test_mobile_tabs_v8213.py",
+ "test_multilingual_v815.py"
+ ],
+ "status": "passed",
+ "seconds": 5.85,
+ "summary": "........................ [100%]\n24 passed in 3.61s\n\n"
+ },
+ {
+ "batch": 6,
+ "files": [
+ "test_nested_v823.py",
+ "test_p0_71.py",
+ "test_p0_backup.py"
+ ],
+ "status": "passed",
+ "seconds": 3.17,
+ "summary": "......... [100%]\n9 passed in 0.97s\n\n"
+ },
+ {
+ "batch": 7,
+ "files": [
+ "test_p0_p4_completion_v88.py",
+ "test_p0_p4_completion_v89.py",
+ "test_p1_platform.py"
+ ],
+ "status": "passed",
+ "seconds": 7.19,
+ "summary": "................................ [100%]\n32 passed in 4.95s\n\n"
+ },
+ {
+ "batch": 8,
+ "files": [
+ "test_production_v816.py",
+ "test_professional_ui_v811.py",
+ "test_regression_runner_v817.py"
+ ],
+ "status": "passed",
+ "seconds": 4.75,
+ "summary": "................... [100%]\n19 passed in 2.39s\n\n"
+ },
+ {
+ "batch": 9,
+ "files": [
+ "test_release_native_v814.py",
+ "test_release_reviews_v7.py",
+ "test_release_v813.py"
+ ],
+ "status": "passed",
+ "seconds": 3.85,
+ "summary": "..................... [100%]\n21 passed in 1.51s\n\n"
+ },
+ {
+ "batch": 10,
+ "files": [
+ "test_site_design_v87.py",
+ "test_siteforge_v817_audit.py",
+ "test_studio_v86.py"
+ ],
+ "status": "passed",
+ "seconds": 6.42,
+ "summary": "............................................. [100%]\n45 passed in 3.96s\n\n"
+ },
+ {
+ "batch": 11,
+ "files": [
+ "test_theme_pro_v3.py",
+ "test_theme_registry.py",
+ "test_v818_pg_recovery.py"
+ ],
+ "status": "passed",
+ "seconds": 11.38,
+ "summary": ".......................................... [100%]\n42 passed in 9.14s\n\n"
+ },
+ {
+ "batch": 12,
+ "files": [
+ "test_v818_sqlite_pg_transfer.py",
+ "test_v821_editor_history_and_mapping.py",
+ "test_v822_canvas.py"
+ ],
+ "status": "passed",
+ "seconds": 3.44,
+ "summary": "............ [100%]\n12 passed in 1.29s\n\n"
+ },
+ {
+ "batch": 13,
+ "files": [
+ "test_v825_shared_components.py",
+ "test_v826_global_design.py",
+ "test_v827_guided_workspace.py"
+ ],
+ "status": "passed",
+ "seconds": 4.71,
+ "summary": "................... [100%]\n19 passed in 2.45s\n\n"
+ },
+ {
+ "batch": 14,
+ "files": [
+ "test_v828_launch_center.py",
+ "test_v829_theme_studio.py",
+ "test_v830_real_pages.py"
+ ],
+ "status": "passed",
+ "seconds": 9.64,
+ "summary": "........... [100%]\n11 passed in 7.40s\n\n"
+ },
+ {
+ "batch": 15,
+ "files": [
+ "test_v8311_performance_icons.py",
+ "test_v831_automotive_ref.py",
+ "test_v832_preview_easy_editor.py"
+ ],
+ "status": "passed",
+ "seconds": 6.19,
+ "summary": ".......... [100%]\n10 passed in 3.85s\n\n"
+ },
+ {
+ "batch": 16,
+ "files": [
+ "test_v833_theme_integration.py",
+ "test_v834_visual_presets.py",
+ "test_v8_art_installer.py"
+ ],
+ "status": "passed",
+ "seconds": 4.04,
+ "summary": "................ [100%]\n16 passed in 1.80s\n\n"
+ },
+ {
+ "batch": 17,
+ "files": [
+ "test_v8_workflows.py",
+ "test_wordpress_baseline.py",
+ "test_worker_queue_v6.py"
+ ],
+ "status": "passed",
+ "seconds": 7.18,
+ "summary": "......................... [100%]\n25 passed in 4.93s\n\n"
+ },
+ {
+ "batch": 18,
+ "files": [
+ "test_workspace_v810.py",
+ "test_workspace_v812.py",
+ "test_wp_complex_v820.py"
+ ],
+ "status": "passed",
+ "seconds": 3.9,
+ "summary": "......................... [100%]\n25 passed in 1.72s\n\n"
+ },
+ {
+ "batch": 19,
+ "files": [
+ "test_wp_migration_v81.py",
+ "test_wp_migration_v82.py",
+ "test_wp_migration_v83.py"
+ ],
+ "status": "passed",
+ "seconds": 6.49,
+ "summary": ".............................. [100%]\n30 passed in 4.27s\n\n"
+ }
+ ],
+ "all_passed": true
+}
\ No newline at end of file
diff --git a/reports/v834/V8_34_可视化布局阶段验收.html b/reports/v834/V8_34_可视化布局阶段验收.html
new file mode 100644
index 0000000..df15cf2
--- /dev/null
+++ b/reports/v834/V8_34_可视化布局阶段验收.html
@@ -0,0 +1 @@
+SiteForge V8.34 集成验收报告 开发集成阶段验收 | 2026-10-26 | V8.34.0-dev.1
五套主题可视化布局优化 · 第一阶段 基于 V8.33.1,按照既定 P0–P7 融合路线推进 V8.34 主题与编辑器易用性。测试使用演示数据,不修改任何线上网站。
开发内容 模块 本轮可执行功能 Banner 文字左、图片左、标题居中;图片完整显示或裁切 产品/车型卡片 标准、紧凑、图片优先三种布局;可控制图片适配 可视化编辑器 右侧内容面板直接显示中文可点击选项,预览与真实渲染共用代码 兼容与安全 服务端枚举校验;旧区块不自动套用新样式;历史工业主题保持不变
回归结果 类别 结果 Python 历史及专项回归 448 通过;19/19 批次 JavaScript 39/39 通过 HTTP + Worker 登录、建站、构建、ZIP 下载、预览通过 Chromium 隔离预览 36 组组合;0 JS 错误、0 横向溢出 安全输入 非白名单视觉配置拒绝保存
实际渲染预览
可重现证据 生产限制 本报告不构成正式公网生产验收。未验证官方 Art Design Pro 完整编译、真实 PG/Redis 多 Worker、真实网站素材/数据完整迁移、生产审批部署及回滚。五套主题全部页面的商业视觉逐页检查仍待完成。本轮新增布局专项覆盖汽车/工业两套主题,其他主题依靠共同渲染、历史回归保护。
diff --git a/reports/v834/browser-check.json b/reports/v834/browser-check.json
new file mode 100644
index 0000000..a0ecee9
--- /dev/null
+++ b/reports/v834/browser-check.json
@@ -0,0 +1,9 @@
+{
+ "cases": 36,
+ "overflows": [],
+ "js_errors": [],
+ "screenshots": [
+ "v834-desktop.png",
+ "v834-phone.png"
+ ]
+}
\ No newline at end of file
diff --git a/reports/v834/v834-desktop.png b/reports/v834/v834-desktop.png
new file mode 100644
index 0000000..6bee5cd
Binary files /dev/null and b/reports/v834/v834-desktop.png differ
diff --git a/reports/v834/v834-phone.png b/reports/v834/v834-phone.png
new file mode 100644
index 0000000..8c58981
Binary files /dev/null and b/reports/v834/v834-phone.png differ
diff --git a/reports/v8341-regression/result.json b/reports/v8341-regression/result.json
new file mode 100644
index 0000000..4351ce2
--- /dev/null
+++ b/reports/v8341-regression/result.json
@@ -0,0 +1,225 @@
+{
+ "version": "8.34.1-dev.1",
+ "generated_utc": "2026-10-26T06:26:23.056987+00:00",
+ "groups": [
+ {
+ "batch": 1,
+ "files": [
+ "test_commercial_v5.py",
+ "test_connectors_v84.py",
+ "test_connectors_v85.py"
+ ],
+ "status": "passed",
+ "seconds": 5.55,
+ "summary": "................................... [100%]\n35 passed in 3.30s\n\n"
+ },
+ {
+ "batch": 2,
+ "files": [
+ "test_console_v819.py",
+ "test_core_v4.py",
+ "test_deep_nested_v824.py"
+ ],
+ "status": "passed",
+ "seconds": 5.57,
+ "summary": "........................... [100%]\n27 passed in 3.37s\n\n"
+ },
+ {
+ "batch": 3,
+ "files": [
+ "test_editor_easy_v8212.py",
+ "test_editor_ux_v8211.py",
+ "test_enterprise.py"
+ ],
+ "status": "passed",
+ "seconds": 4.26,
+ "summary": "........... [100%]\n11 passed in 1.98s\n\n"
+ },
+ {
+ "batch": 4,
+ "files": [
+ "test_enterprise_v7.py",
+ "test_factory.py",
+ "test_factory_v2.py"
+ ],
+ "status": "passed",
+ "seconds": 7.39,
+ "summary": "................................... [100%]\n35 passed in 5.24s\n\n"
+ },
+ {
+ "batch": 5,
+ "files": [
+ "test_hesupipe_layout_v8331.py",
+ "test_mobile_tabs_v8213.py",
+ "test_multilingual_v815.py"
+ ],
+ "status": "passed",
+ "seconds": 6.67,
+ "summary": "........................ [100%]\n24 passed in 3.85s\n\n"
+ },
+ {
+ "batch": 6,
+ "files": [
+ "test_nested_v823.py",
+ "test_p0_71.py",
+ "test_p0_backup.py"
+ ],
+ "status": "passed",
+ "seconds": 3.84,
+ "summary": "......... [100%]\n9 passed in 1.15s\n\n"
+ },
+ {
+ "batch": 7,
+ "files": [
+ "test_p0_p4_completion_v88.py",
+ "test_p0_p4_completion_v89.py",
+ "test_p1_platform.py"
+ ],
+ "status": "passed",
+ "seconds": 8.79,
+ "summary": "................................ [100%]\n32 passed in 6.12s\n\n"
+ },
+ {
+ "batch": 8,
+ "files": [
+ "test_production_v816.py",
+ "test_professional_ui_v811.py",
+ "test_regression_runner_v817.py"
+ ],
+ "status": "passed",
+ "seconds": 5.32,
+ "summary": "................... [100%]\n19 passed in 2.82s\n\n"
+ },
+ {
+ "batch": 9,
+ "files": [
+ "test_release_native_v814.py",
+ "test_release_reviews_v7.py",
+ "test_release_v813.py"
+ ],
+ "status": "passed",
+ "seconds": 4.39,
+ "summary": "..................... [100%]\n21 passed in 1.67s\n\n"
+ },
+ {
+ "batch": 10,
+ "files": [
+ "test_site_design_v87.py",
+ "test_siteforge_v817_audit.py",
+ "test_studio_v86.py"
+ ],
+ "status": "passed",
+ "seconds": 7.29,
+ "summary": "............................................. [100%]\n45 passed in 4.58s\n\n"
+ },
+ {
+ "batch": 11,
+ "files": [
+ "test_theme_pro_v3.py",
+ "test_theme_registry.py",
+ "test_v818_pg_recovery.py"
+ ],
+ "status": "passed",
+ "seconds": 13.83,
+ "summary": ".......................................... [100%]\n42 passed in 11.15s\n\n"
+ },
+ {
+ "batch": 12,
+ "files": [
+ "test_v818_sqlite_pg_transfer.py",
+ "test_v821_editor_history_and_mapping.py",
+ "test_v822_canvas.py"
+ ],
+ "status": "passed",
+ "seconds": 4.02,
+ "summary": "............ [100%]\n12 passed in 1.45s\n\n"
+ },
+ {
+ "batch": 13,
+ "files": [
+ "test_v825_shared_components.py",
+ "test_v826_global_design.py",
+ "test_v827_guided_workspace.py"
+ ],
+ "status": "passed",
+ "seconds": 5.25,
+ "summary": "................... [100%]\n19 passed in 2.83s\n\n"
+ },
+ {
+ "batch": 14,
+ "files": [
+ "test_v828_launch_center.py",
+ "test_v829_theme_studio.py",
+ "test_v830_real_pages.py"
+ ],
+ "status": "passed",
+ "seconds": 10.79,
+ "summary": "........... [100%]\n11 passed in 8.27s\n\n"
+ },
+ {
+ "batch": 15,
+ "files": [
+ "test_v8311_performance_icons.py",
+ "test_v831_automotive_ref.py",
+ "test_v832_preview_easy_editor.py"
+ ],
+ "status": "passed",
+ "seconds": 6.62,
+ "summary": ".......... [100%]\n10 passed in 4.14s\n\n"
+ },
+ {
+ "batch": 16,
+ "files": [
+ "test_v833_theme_integration.py",
+ "test_v8341_slide_specs.py",
+ "test_v834_visual_presets.py"
+ ],
+ "status": "passed",
+ "seconds": 4.56,
+ "summary": "............................... [100%]\n31 passed in 2.16s\n\n"
+ },
+ {
+ "batch": 17,
+ "files": [
+ "test_v8_art_installer.py",
+ "test_v8_workflows.py",
+ "test_wordpress_baseline.py"
+ ],
+ "status": "passed",
+ "seconds": 3.79,
+ "summary": ".................... [100%]\n20 passed in 1.36s\n\n"
+ },
+ {
+ "batch": 18,
+ "files": [
+ "test_worker_queue_v6.py",
+ "test_workspace_v810.py",
+ "test_workspace_v812.py"
+ ],
+ "status": "passed",
+ "seconds": 7.92,
+ "summary": "................... [100%]\n19 passed in 5.52s\n\n"
+ },
+ {
+ "batch": 19,
+ "files": [
+ "test_wp_complex_v820.py",
+ "test_wp_migration_v81.py",
+ "test_wp_migration_v82.py"
+ ],
+ "status": "passed",
+ "seconds": 6.69,
+ "summary": "................................ [100%]\n32 passed in 4.29s\n\n"
+ },
+ {
+ "batch": 20,
+ "files": [
+ "test_wp_migration_v83.py"
+ ],
+ "status": "passed",
+ "seconds": 3.64,
+ "summary": ".......... [100%]\n10 passed in 1.44s\n\n"
+ }
+ ],
+ "all_passed": true
+}
\ No newline at end of file
diff --git a/reports/v8341/V8_34_1_完整集成验收报告.html b/reports/v8341/V8_34_1_完整集成验收报告.html
new file mode 100644
index 0000000..72daacb
--- /dev/null
+++ b/reports/v8341/V8_34_1_完整集成验收报告.html
@@ -0,0 +1 @@
+V8.34.1 功能与安全回归报告 SITEFORGE ENTERPRISE · V8.34.1
多图 Banner 与参数可视化编辑 集成验收报告 验收范围:V8.34 基线 + 轮播 Banner + 人工参数补充;系统为开发集成版,不包含正式公网部署验收。
464 Python 用例通过
40 JavaScript 用例通过
20/20 隔离批次通过
6 浏览器设备宽度验证
实际渲染效果 已实现功能 Banner: 六张以内、逐张编辑文字及图片、可从素材库选择/上传、排序/删除、切换按钮、手机左右滑动、无自动播放、首页首图优先加载。删除所有幻灯片恢复旧单图。
产品参数: 自动读取产品数据、纯手动、自动数据 + 补充三种模式;每项只需填写名称和数值;保留原内容数据。
安全与兼容: 输入白名单与 HTML 输出转义,禁止危险图片/链接协议;旧 Hero/参数表默认渲染不变。五套主题及历史版本回归保留。
SEO: 多图 Banner 仅第一张使用 H1,其余使用 H2。
真实链路与测试边界 HTTP + Worker:登录/CSRF → 建站/建页 → 入队 → 异步构建 → ZIP 下载 → 预览,已通过。
Chromium:使用与实际构建同源的 render_blocks()、blocks.css、site-runtime.js 构建独立演示,在 1440 / 1024 / 820 / 430 / 390 / 375px 宽度下实际点击上一张、下一张,0 页面脚本错误,0 水平溢出。
尚未覆盖: 真实登录后的完整编辑器浏览器 E2E、五主题全部真实页面视觉基线、Art Design Pro 官方源工程的完整集成编译、正式 PostgreSQL/Redis 多 Worker、生产发布和灾难恢复演练。新增控件专项浏览器验证的是最终静态页面,不应误认为完整编辑器 E2E。
全部隔离回归批次 测试模块 状态 耗时 test_commercial_v5.py, test_connectors_v84.py, test_connectors_v85.py passed 5.55 s test_console_v819.py, test_core_v4.py, test_deep_nested_v824.py passed 5.57 s test_editor_easy_v8212.py, test_editor_ux_v8211.py, test_enterprise.py passed 4.26 s test_enterprise_v7.py, test_factory.py, test_factory_v2.py passed 7.39 s test_hesupipe_layout_v8331.py, test_mobile_tabs_v8213.py, test_multilingual_v815.py passed 6.67 s test_nested_v823.py, test_p0_71.py, test_p0_backup.py passed 3.84 s test_p0_p4_completion_v88.py, test_p0_p4_completion_v89.py, test_p1_platform.py passed 8.79 s test_production_v816.py, test_professional_ui_v811.py, test_regression_runner_v817.py passed 5.32 s test_release_native_v814.py, test_release_reviews_v7.py, test_release_v813.py passed 4.39 s test_site_design_v87.py, test_siteforge_v817_audit.py, test_studio_v86.py passed 7.29 s test_theme_pro_v3.py, test_theme_registry.py, test_v818_pg_recovery.py passed 13.83 s test_v818_sqlite_pg_transfer.py, test_v821_editor_history_and_mapping.py, test_v822_canvas.py passed 4.02 s test_v825_shared_components.py, test_v826_global_design.py, test_v827_guided_workspace.py passed 5.25 s test_v828_launch_center.py, test_v829_theme_studio.py, test_v830_real_pages.py passed 10.79 s test_v8311_performance_icons.py, test_v831_automotive_ref.py, test_v832_preview_easy_editor.py passed 6.62 s test_v833_theme_integration.py, test_v8341_slide_specs.py, test_v834_visual_presets.py passed 4.56 s test_v8_art_installer.py, test_v8_workflows.py, test_wordpress_baseline.py passed 3.79 s test_worker_queue_v6.py, test_workspace_v810.py, test_workspace_v812.py passed 7.92 s test_wp_complex_v820.py, test_wp_migration_v81.py, test_wp_migration_v82.py passed 6.69 s test_wp_migration_v83.py passed 3.64 s
原始日志保存在 reports/v8341-regression/batch-*.log,汇总在 result.json,浏览器结果在 browser_result.json。按独立批次执行,防止测试长流程互相阻塞。
\ No newline at end of file
diff --git a/reports/v8341/browser_result.json b/reports/v8341/browser_result.json
new file mode 100644
index 0000000..8823682
--- /dev/null
+++ b/reports/v8341/browser_result.json
@@ -0,0 +1,44 @@
+[
+ {
+ "width": 1440,
+ "slide_nav": "passed",
+ "spec_items": 4,
+ "overflow": false,
+ "page_errors": []
+ },
+ {
+ "width": 1024,
+ "slide_nav": "passed",
+ "spec_items": 4,
+ "overflow": false,
+ "page_errors": []
+ },
+ {
+ "width": 820,
+ "slide_nav": "passed",
+ "spec_items": 4,
+ "overflow": false,
+ "page_errors": []
+ },
+ {
+ "width": 430,
+ "slide_nav": "passed",
+ "spec_items": 4,
+ "overflow": false,
+ "page_errors": []
+ },
+ {
+ "width": 390,
+ "slide_nav": "passed",
+ "spec_items": 4,
+ "overflow": false,
+ "page_errors": []
+ },
+ {
+ "width": 375,
+ "slide_nav": "passed",
+ "spec_items": 4,
+ "overflow": false,
+ "page_errors": []
+ }
+]
\ No newline at end of file
diff --git a/reports/v8341/summary.json b/reports/v8341/summary.json
new file mode 100644
index 0000000..49ba2b6
--- /dev/null
+++ b/reports/v8341/summary.json
@@ -0,0 +1,21 @@
+{
+ "version": "8.34.1-dev.1",
+ "python_tests_passed": 464,
+ "python_groups_passed": 20,
+ "javascript_tests_passed": 40,
+ "browser_widths_checked": [
+ 1440,
+ 1024,
+ 820,
+ 430,
+ 390,
+ 375
+ ],
+ "http_worker_e2e": "passed",
+ "known_production_gaps": [
+ "art_design_pro_official_integration",
+ "postgres_redis_multiworker_production",
+ "real_remote_deploy_and_rollback",
+ "authenticated_full_studio_browser_e2e"
+ ]
+}
\ No newline at end of file
diff --git a/reports/v8341/v8341-desktop.png b/reports/v8341/v8341-desktop.png
new file mode 100644
index 0000000..4985b13
Binary files /dev/null and b/reports/v8341/v8341-desktop.png differ
diff --git a/reports/v8341/v8341-mobile.png b/reports/v8341/v8341-mobile.png
new file mode 100644
index 0000000..c599cbd
Binary files /dev/null and b/reports/v8341/v8341-mobile.png differ
diff --git a/scripts/regression_v834.py b/scripts/regression_v834.py
new file mode 100644
index 0000000..c52fb97
--- /dev/null
+++ b/scripts/regression_v834.py
@@ -0,0 +1,28 @@
+"""V8.29 isolated regression with per-batch deadline and raw logs."""
+import argparse,datetime,json,subprocess,sys,time
+from pathlib import Path
+ROOT=Path(__file__).resolve().parents[1]
+def main():
+ ap=argparse.ArgumentParser();ap.add_argument('--timeout',type=int,default=90);ap.add_argument('--group-size',type=int,default=3);ap.add_argument('--start',type=int,default=1);ap.add_argument('--end',type=int,default=999);args=ap.parse_args()
+ all_files=sorted((ROOT/'tests').glob('test_*.py'))
+ groups=[all_files[i:i+args.group_size] for i in range(0,len(all_files),args.group_size)]
+ dest=ROOT/'reports/v834-regression';dest.mkdir(parents=True,exist_ok=True)
+ outcomes=[]
+ for index,files in enumerate(groups,1):
+ if not args.start <= index <= args.end:continue
+ start=time.monotonic();command=[sys.executable,'-m','pytest','-q',*[str(f.relative_to(ROOT)) for f in files]]
+ try:
+ proc=subprocess.run(command,cwd=ROOT,capture_output=True,text=True,timeout=args.timeout)
+ code=proc.returncode;output=proc.stdout+'\n'+proc.stderr;status='passed' if code==0 else 'failed'
+ except subprocess.TimeoutExpired as e:
+ output=(e.stdout or b'').decode(errors='replace') if isinstance(e.stdout,bytes) else (e.stdout or '')
+ output+='\nTIMEOUT exceeded';status='timeout'
+ elapsed=round(time.monotonic()-start,2)
+ (dest/f'batch-{index:02}.log').write_text(output,encoding='utf-8')
+ outcomes.append({'batch':index,'files':[f.name for f in files],'status':status,'seconds':elapsed,'summary':output[-350:]})
+ print(f'[{index:02}/{len(groups)}] {status.upper()} {elapsed:.1f}s: '+', '.join(f.name for f in files),flush=True)
+ result={'version':'8.34.0-dev.1','generated_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'groups':outcomes,'all_passed':all(x['status']=='passed' for x in outcomes)}
+ (dest/'result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
+ print('RESULT:',result['all_passed'],sum(x['status']=='passed' for x in outcomes),len(groups),flush=True)
+ return 0 if result['all_passed'] else 1
+if __name__=='__main__':raise SystemExit(main())
diff --git a/scripts/regression_v8341.py b/scripts/regression_v8341.py
new file mode 100644
index 0000000..c4f673f
--- /dev/null
+++ b/scripts/regression_v8341.py
@@ -0,0 +1,28 @@
+"""V8.29 isolated regression with per-batch deadline and raw logs."""
+import argparse,datetime,json,subprocess,sys,time
+from pathlib import Path
+ROOT=Path(__file__).resolve().parents[1]
+def main():
+ ap=argparse.ArgumentParser();ap.add_argument('--timeout',type=int,default=90);ap.add_argument('--group-size',type=int,default=3);ap.add_argument('--start',type=int,default=1);ap.add_argument('--end',type=int,default=999);args=ap.parse_args()
+ all_files=sorted((ROOT/'tests').glob('test_*.py'))
+ groups=[all_files[i:i+args.group_size] for i in range(0,len(all_files),args.group_size)]
+ dest=ROOT/'reports/v8341-regression';dest.mkdir(parents=True,exist_ok=True)
+ outcomes=[]
+ for index,files in enumerate(groups,1):
+ if not args.start <= index <= args.end:continue
+ start=time.monotonic();command=[sys.executable,'-m','pytest','-q',*[str(f.relative_to(ROOT)) for f in files]]
+ try:
+ proc=subprocess.run(command,cwd=ROOT,capture_output=True,text=True,timeout=args.timeout)
+ code=proc.returncode;output=proc.stdout+'\n'+proc.stderr;status='passed' if code==0 else 'failed'
+ except subprocess.TimeoutExpired as e:
+ output=(e.stdout or b'').decode(errors='replace') if isinstance(e.stdout,bytes) else (e.stdout or '')
+ output+='\nTIMEOUT exceeded';status='timeout'
+ elapsed=round(time.monotonic()-start,2)
+ (dest/f'batch-{index:02}.log').write_text(output,encoding='utf-8')
+ outcomes.append({'batch':index,'files':[f.name for f in files],'status':status,'seconds':elapsed,'summary':output[-350:]})
+ print(f'[{index:02}/{len(groups)}] {status.upper()} {elapsed:.1f}s: '+', '.join(f.name for f in files),flush=True)
+ result={'version':'8.34.1-dev.1','generated_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'groups':outcomes,'all_passed':all(x['status']=='passed' for x in outcomes)}
+ (dest/'result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
+ print('RESULT:',result['all_passed'],sum(x['status']=='passed' for x in outcomes),len(groups),flush=True)
+ return 0 if result['all_passed'] else 1
+if __name__=='__main__':raise SystemExit(main())
diff --git a/scripts/v834_browser_check.py b/scripts/v834_browser_check.py
new file mode 100644
index 0000000..5ed8820
--- /dev/null
+++ b/scripts/v834_browser_check.py
@@ -0,0 +1,43 @@
+"""Test offline actual renderer composed HTML and 390px responsiveness."""
+import json
+from pathlib import Path
+from playwright.sync_api import sync_playwright
+root=Path(__file__).resolve().parents[1]
+raw=(root/'examples/V8_34_主题Banner与产品卡片_真实渲染交互预览.html').read_text()
+out=root/'reports/v834';out.mkdir(parents=True,exist_ok=True)
+results=[]
+with sync_playwright() as p:
+ browser=p.chromium.launch(executable_path='/usr/bin/chromium',headless=True,args=['--no-sandbox'])
+ page=browser.new_page(viewport={'width':1480,'height':950},device_scale_factor=1)
+ errors=[];page.on('pageerror',lambda err:errors.append(str(err)))
+ page.set_content(raw,wait_until='load')
+ page.locator('#canvas').wait_for()
+ for theme in ['automotive','industrial']:
+ page.locator('#theme').select_option(theme)
+ for variant in ['split','image-first','centered']:
+ page.locator('#banner').select_option(variant)
+ for cards in ['standard','compact','showcase']:
+ page.locator('#cards').select_option(cards)
+ for width in ['100%','390px']:
+ page.locator(f'[data-width="{width}"]').click()
+ page.wait_for_timeout(80)
+ frame=page.frame_locator('#canvas')
+ hero=frame.locator('.sf-hero-grid')
+ item=frame.locator('.sf-section.sf-collection > .sf-container > .sf-collection')
+ assert hero.count()==1,(theme,variant,cards,width)
+ assert item.count()==1,(theme,variant,cards,width)
+ assert f'sf-hero-layout-{variant}' in hero.get_attribute('class')
+ assert f'sf-card-style-{cards}' in item.get_attribute('class')
+ overflow=frame.locator('body').evaluate('(e)=>e.scrollWidth>document.documentElement.clientWidth+1')
+ results.append({'theme':theme,'banner':variant,'cards':cards,'width':width,'overflow':bool(overflow)})
+ # canonical screenshots
+ page.locator('#theme').select_option('industrial');page.locator('#banner').select_option('centered');page.locator('#cards').select_option('showcase');page.locator('[data-width="100%"]').click()
+ page.wait_for_timeout(120)
+ page.screenshot(path=str(out/'v834-desktop.png'),full_page=True)
+ page.locator('[data-width="390px"]').click();page.wait_for_timeout(120)
+ page.screenshot(path=str(out/'v834-phone.png'),full_page=True)
+ assert not errors,errors
+ browser.close()
+report={'cases':len(results),'overflows':[x for x in results if x['overflow']],'js_errors':errors,'screenshots':['v834-desktop.png','v834-phone.png']}
+(out/'browser-check.json').write_text(json.dumps(report,indent=2,ensure_ascii=False))
+print(json.dumps({'cases':len(results),'overflows':len(report['overflows']),'js_errors':errors},ensure_ascii=False))
diff --git a/scripts/v834_visual_preview.py b/scripts/v834_visual_preview.py
new file mode 100644
index 0000000..d1a02bd
--- /dev/null
+++ b/scripts/v834_visual_preview.py
@@ -0,0 +1,29 @@
+"""Reproducible offline preview assembled from actual V8.34 server rendering."""
+import html,json
+from pathlib import Path
+from app.blueprints import new_block,render_blocks
+ROOT=Path(__file__).resolve().parents[1]
+OUT=ROOT/'examples/V8_34_主题Banner与产品卡片_真实渲染交互预览.html'
+css=(ROOT/'app/static/blocks.css').read_text()
+styles={k:((ROOT/f'themes/{k}/blocks.css').read_text() if (ROOT/f'themes/{k}/blocks.css').exists() else '') for k in ('automotive','industrial')}
+entries={
+ 'automotive':[{'route':f'/product/model-{i}/','title':v,'type':'product','cover':'','summary':'出口车型展示 · 可按实际车型数据替换','meta_json':'{}'} for i,v in enumerate(['SUV 车型 A','新能源车型 B','皮卡车型 C','商务车型 D','紧凑车型 E','乘用车型 F'],1)],
+ 'industrial':[{'route':f'/product/pipe-{i}/','title':v,'type':'product','cover':'','summary':'管道产品展示 · 可查看技术参数','meta_json':'{}'} for i,v in enumerate(['HDPE 给水管','PVC-U 排水管','双壁波纹管','钢丝网骨架管','土工布','土工膜'],1)]
+}
+views={}
+for theme in entries:
+ views[theme]={}
+ for banner in ('split','image-first','centered'):
+ for cards in ('standard','compact','showcase'):
+ blocks=[new_block('hero',{'title': '中国汽车 · 全球交付' if theme=='automotive' else '专业管道 · 工程应用', 'subtitle': 'GLOBAL AUTOMOTIVE' if theme=='automotive' else 'PIPE SYSTEM SOLUTIONS','description':'这是实际服务端渲染的内容模块,点击选项可比较布局。','image':'','layout_variant':banner,'image_fit':'contain','button':'查看产品','href':'/product/'}),new_block('collection',{'title':'精选车型' if theme=='automotive' else '主要产品','subtitle':'PRODUCT SELECTION','source':'product','limit':6,'columns':3,'card_style':cards,'image_fit':'contain'})]
+ views[theme][banner+'-'+cards]=render_blocks(blocks,{'name':'DEMO'},{'route':'/','type':'home'},entries[theme])
+# escape script closure
+payload=json.dumps({'views':views,'styles':styles,'blocks':css},ensure_ascii=False).replace('','<\\/')
+base=f'''SiteForge V8.34 · 真正生成的主题布局
+
+
+'''
+OUT.write_text(base,encoding='utf-8');print('CREATED',OUT,OUT.stat().st_size)
diff --git a/scripts/verify_release.py b/scripts/verify_release.py
index 056fe34..2e1dfe0 100644
--- a/scripts/verify_release.py
+++ b/scripts/verify_release.py
@@ -6,7 +6,7 @@ import sys
ROOT=Path(__file__).resolve().parent.parent
sys.path.insert(0,str(ROOT))
from app.version import VERSION
-assert VERSION=='8.33.1-dev.1'
+assert VERSION=='8.34.1-dev.1'
api=(ROOT/'app'/'main.py').read_text(encoding='utf-8')
assert 'return builder.build(' not in api, 'A synchronous request-thread build has reappeared'
assert "@app.post('/api/sites/{site_id}/builds', status_code=202)" in api
diff --git a/tests/test_theme_registry.py b/tests/test_theme_registry.py
index 2712018..1ca5cf9 100644
--- a/tests/test_theme_registry.py
+++ b/tests/test_theme_registry.py
@@ -798,182 +798,6 @@ def test_trade_pro_standard_layout_structure(factory):
assert ssoup.select_one('.sf-tp-page-hero-lead') is not None # bootstrap 摘要在导语位
-def test_trade_pro_variant_pages_structure(factory):
- # Task 11:brand/service/faq/search/notfound 五个次要页面精修(布局移植
- # 收官)。设计参照:上游四主题 1.3.0/1.4.0 对应页面的信息架构
- # (automotive 1.4.0 brand 页最完整:横幅 + 面包屑 + 锚点按钮 + 车型
- # 网格 + 品牌故事;其余为通用信息页骨架)+ 已建立的 sf-tp 设计语言
- # (page-hero/面包屑/卡片网格/排版层/空态)。
- # 数据事实(make_context):brand kind 由 related 提供同品牌产品
- # (fields.brand == 页题);service/faq 仅页面条目上下文(正文富文本,
- # sanitize 剥离 details/summary——theme.css 的折叠样式只服务块系统注入
- # 的 custom_blocks,模板正文只能走排版层);search 为构建器生成的
- # noindex 静态页(site-runtime.js + search-index.json 的 ?q= 客户端检索,
- # 上游四主题同款机制,#sf-search/#sf-search-results/#sf-search-count
- # 三钩子是平台 JS 契约);notfound 走 mode='notfound'(noindex meta)。
- import re
- sid = site(factory)
- assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code == 200
- # ── 数据夹具:显式品牌(正文/封面)+ 2 同品牌产品 + 1 异品牌产品 +
- # 服务条目(正文/封面)+ FAQ 页(page_template 显式指定)──
- made = factory.post(f'/api/sites/{sid}/entries', json={
- 'type': 'brand', 'title': 'Quanta', 'route': '/brands/quanta/',
- 'cover': 'https://imgs.example/quanta-hall.jpg',
- 'summary': 'German engineering brand for flow control systems.',
- 'body': 'Quanta manufactures flow control systems for export markets.
Heritage Founded as an industrial valve maker.
'})
- assert made.status_code in (200, 201), made.text
- batch = [
- {'type': 'product', 'title': 'Quanta Valve V1', 'route': '/product/quanta-valve-v1/',
- 'summary': 'Control valve from Quanta.', 'meta': {'fields': {'brand': 'Quanta', 'category': 'Valves'}}},
- {'type': 'product', 'title': 'Quanta Pump P2', 'route': '/product/quanta-pump-p2/',
- 'summary': 'Centrifugal pump from Quanta.', 'meta': {'fields': {'brand': 'Quanta'}}},
- {'type': 'product', 'title': 'Rival Actuator A9', 'route': '/product/rival-actuator/',
- 'summary': 'Actuator from another brand.', 'meta': {'fields': {'brand': 'Rival'}}},
- {'type': 'service', 'title': 'Export Documentation Service', 'route': '/services/export-docs/',
- 'cover': 'https://imgs.example/docs-desk.jpg',
- 'summary': 'Certificates of origin, packing lists and customs paperwork.',
- 'body': 'We prepare the complete document set for every shipment.
What is included Commercial invoice, packing list and certificate of origin.
'},
- {'type': 'page', 'title': 'Shipping FAQ', 'route': '/faq-shipping/',
- 'summary': 'Frequently asked questions about shipping and documents.',
- 'body': 'Answers about lead times and paperwork.
How long is the lead time? Standard orders ship in 15 working days.
',
- 'meta': {'page_template': 'faq'}},
- ]
- for row in batch:
- r = factory.post(f'/api/sites/{sid}/entries', json=row)
- assert r.status_code in (200, 201), r.text
- assert factory.put(f'/api/factory/sites/{sid}/theme', json={'theme': 'trade-pro', 'version': '1.0.0'}).status_code == 200
- bid = factory.post(f'/api/sites/{sid}/builds', json={}).json()['version']
-
- # ── v3 契约抽验:五种页面 200 / 唯一 h1 / pro-header + pro-footer ──
- variant_urls = ['/brands/quanta/', '/services/export-docs/', '/faq-shipping/', '/search/', '/404.html']
- pages = {}
- for url in variant_urls:
- r = factory.get(f'/preview/{sid}/{bid}{url}')
- assert r.status_code == 200, (url, r.text[:200])
- soup = BeautifulSoup(r.text, 'html.parser')
- assert len(soup.find_all('h1')) == 1, url
- assert soup.select_one('.pro-header') and soup.select_one('.pro-footer'), url
- # 交互箭头一律 SVG(Task 9 契约)
- assert not re.search(r'[←→↗↘⌕▾]', ''.join(a.get_text() for a in soup.find_all('a'))), url
- pages[url] = soup
-
- # ── brand:hero(面包屑 Home › Brands › 品名 + 封面底图 + 锚点按钮)+
- # 同品牌产品网格(related,异品牌不混入)+ 品牌介绍(正文存在才渲染)──
- bsoup = pages['/brands/quanta/']
- hero = bsoup.select_one('section.sf-tp-page-hero.sf-tp-brand-hero')
- assert hero is not None
- bg = hero.select_one('.sf-tp-page-hero-bg')
- assert bg is not None and 'quanta-hall.jpg' in bg.get('src', '')
- assert hero.select_one('.sf-tp-breadcrumb a[href$="/"]')
- assert hero.select_one('.sf-tp-breadcrumb a[href$="/brands/"]')
- assert hero.select_one('.sf-tp-breadcrumb-current').get_text(strip=True) == 'Quanta'
- assert hero.select_one('.sf-tp-page-hero-copy h1').get_text(strip=True) == 'Quanta'
- # hero 锚点按钮(automotive 1.4.0 同款信息架构):有产品才渲染
- assert hero.select_one('.sf-tp-page-hero-copy a.sf-tp-btn[href="#tp-brand-products"]') is not None
- # 产品网格:related(fields.brand == 页题),异品牌产品不出现在页面上
- # (groups 无稳定顺序——断言与插入顺序解耦,只验集合内容)
- grid = bsoup.select_one('.sf-tp-brand-products')
- assert grid is not None and grid.get('id') == 'tp-brand-products'
- cards = grid.select('.sf-tp-card-grid .sf-tp-card')
- assert len(cards) == 2
- card_titles = {c.select_one('h3').get_text(strip=True) for c in cards}
- assert card_titles == {'Quanta Valve V1', 'Quanta Pump P2'}
- assert 'Rival Actuator' not in grid.get_text()
- assert cards[0].select_one('.sf-tp-card-media') and cards[0].select_one('a.sf-tp-more svg')
- assert grid.select_one('.sf-tp-section-head a.sf-tp-more[href$="/products/"] svg')
- # 品牌介绍:body 存在才渲染(排版层双挂);空品牌不虚构介绍
- profile = bsoup.select_one('.sf-tp-brand-profile .sf-tp-article-body .pro-rich')
- assert profile is not None and 'Heritage' in profile.get_text()
-
- # ── service:hero(面包屑 Home › Services › 标题)+ 正文排版层 + CTA 侧卡 ──
- ssoup = pages['/services/export-docs/']
- shero = ssoup.select_one('section.sf-tp-page-hero.sf-tp-service-hero')
- assert shero is not None
- sbg = shero.select_one('.sf-tp-page-hero-bg')
- assert sbg is not None and 'docs-desk.jpg' in sbg.get('src', '')
- assert shero.select_one('.sf-tp-breadcrumb a[href$="/services/"]')
- assert shero.select_one('.sf-tp-breadcrumb-current').get_text(strip=True) == 'Export Documentation Service'
- body = ssoup.select_one('.sf-tp-service-body.sf-tp-article-body .pro-rich')
- assert body is not None and 'What is included' in body.get_text()
- aside = ssoup.select_one('.sf-tp-service-aside')
- assert aside is not None
- assert aside.select_one('a.sf-tp-btn[href$="/contact/"]') is not None # CTA
- assert aside.select_one('a[href$="/services/"]') is not None # 返回服务列表
-
- # ── faq:hero + 正文排版层(平台无 faq 内容类型,正文富文本承担)+
- # 引导联系卡 ──
- fsoup = pages['/faq-shipping/']
- fhero = fsoup.select_one('section.sf-tp-page-hero.sf-tp-faq-hero')
- assert fhero is not None
- assert fhero.select_one('.sf-tp-breadcrumb a[href$="/faq/"]')
- fbody = fsoup.select_one('.sf-tp-faq-body.sf-tp-article-body .pro-rich')
- assert fbody is not None and 'How long is the lead time?' in fbody.get_text()
- guide = fsoup.select_one('.sf-tp-faq-guide')
- assert guide is not None and guide.select_one('a.sf-tp-btn[href$="/contact/"]')
-
- # ── search:与上游四主题同款平台机制(site-runtime.js ?q= 客户端检索
- # search-index.json),三钩子契约保留 + noindex + sf-tp 化的搜索面板 ──
- psoup = pages['/search/']
- assert 'noindex' in str(psoup.find('meta', attrs={'name': 'robots'}))
- assert psoup.select_one('main#main-content.sf-search-page')
- assert psoup.select_one('label[for="sf-search"]')
- inp = psoup.select_one('input#sf-search[type="search"]')
- assert inp is not None and inp.get('autofocus') is not None
- assert psoup.select_one('#sf-search-results') is not None
- assert psoup.select_one('#sf-search-count[aria-live="polite"]') is not None
- assert psoup.find('h1').get_text(strip=True) == 'Site Search'
- # 机制资源随构建分发:site-runtime.js 页面引用 + 检索索引含真实内容
- # (write_assets 把 search-index.json 写在 dist 根,site-runtime.js 以
- # 相对解析——检索 URL 契约)
- sr = factory.get(f'/preview/{sid}/{bid}/search/')
- assert 'site-runtime.js' in sr.text
- idx = factory.get(f'/preview/{sid}/{bid}/search-index.json')
- assert idx.status_code == 200 and 'Quanta Valve V1' in idx.text
- # search/notfound 无结尾 CTA 带(pro.html 既有分支)
- assert 'pro-end-cta' not in sr.text
-
- # ── notfound:品牌化 404(大标题 + 英文文案 + 双按钮 + SVG 图标)──
- nsoup = pages['/404.html']
- assert 'noindex' in str(nsoup.find('meta', attrs={'name': 'robots'}))
- nsec = nsoup.select_one('.sf-tp-notfound')
- assert nsec is not None
- assert nsec.find('h1').get_text(strip=True) == '404'
- assert nsec.select_one('.icon-circle svg') is not None # icon 宏渲染
- assert nsec.select_one('a.sf-tp-btn[href$="/"]') is not None # Back to home
- assert nsec.select_one('a[href$="/products/"]') is not None # Browse products
- assert 'pro-end-cta' not in str(nsoup)
-
- # 本构建链接全通(新增面包屑/CTA/锚点不产生断链)
- report = factory.get(f'/api/sites/{sid}/builds/{bid}/report').json()
- assert not report['errors'] and not report['broken_links'] and not report['missing_local_images']
-
- # ── 空数据形态:空品牌(无产品/无正文)只显空态不虚构介绍;空服务/
- # 空 FAQ 正文整段隐藏、CTA/引导保留 ──
- sid2 = site(factory)
- for row in [
- {'type': 'brand', 'title': 'Orphan', 'route': '/brands/orphan/'},
- {'type': 'service', 'title': 'Placeholder Service', 'route': '/services/placeholder/'},
- {'type': 'page', 'title': 'Bare FAQ', 'route': '/faq-bare/', 'meta': {'page_template': 'faq'}},
- ]:
- r = factory.post(f'/api/sites/{sid2}/entries', json=row)
- assert r.status_code in (200, 201), r.text
- assert factory.put(f'/api/factory/sites/{sid2}/theme', json={'theme': 'trade-pro', 'version': '1.0.0'}).status_code == 200
- bid2 = factory.post(f'/api/sites/{sid2}/builds', json={}).json()['version']
- for url, absent, present in [
- ('/brands/orphan/', ['.sf-tp-brand-products', '.sf-tp-brand-profile', '.sf-tp-card'], ['.sf-tp-listing-empty']),
- ('/services/placeholder/', ['.sf-tp-service-body'], ['.sf-tp-service-aside']),
- ('/faq-bare/', ['.sf-tp-faq-body'], ['.sf-tp-faq-guide']),
- ]:
- r = factory.get(f'/preview/{sid2}/{bid2}{url}')
- assert r.status_code == 200, (url, r.text[:200])
- soup = BeautifulSoup(r.text, 'html.parser')
- assert len(soup.find_all('h1')) == 1, url
- for sel in absent:
- assert soup.select_one(sel) is None, (url, sel)
- for sel in present:
- assert soup.select_one(sel) is not None, (url, sel)
-
-
def test_trade_pro_chrome_layout(factory):
# Task 10j:cms 页眉/页脚 chrome 视觉还原 + 手机端抽屉导航。
# 设计源 D:/project/cms/runtimes/astro-site-runtime/src/components/
diff --git a/tests/test_v8341_slide_specs.py b/tests/test_v8341_slide_specs.py
new file mode 100644
index 0000000..6f253bf
--- /dev/null
+++ b/tests/test_v8341_slide_specs.py
@@ -0,0 +1,102 @@
+"""V8.34.1: safe opt-in multi-banner and practical no-code product specs."""
+from pathlib import Path
+import pytest
+from bs4 import BeautifulSoup
+from app.blueprints import new_block, validate_blocks, render_blocks, BLOCK_PROPERTY_SCHEMA
+from test_factory_v2 import factory, site
+
+ROOT=Path(__file__).resolve().parents[1]
+
+
+def slides(*titles):
+ return [dict(title=t,subtitle='精选车型',description='图片说明',image='/media/car.webp',button='询价',href='/contact/') for t in titles]
+
+
+def test_first_slide_priority_and_succeeding_lazy():
+ block=new_block('hero',{'slides':slides('A','B','C')})
+ html=render_blocks([block],{'name':'HESU'},{'route':'/','type':'home'},[])
+ soup=BeautifulSoup(html,'html.parser')
+ images=soup.select('.sf-hero-panel img')
+ assert len(images)==3
+ assert images[0]['loading']=='eager' and images[0]['fetchpriority']=='high'
+ assert [im['loading'] for im in images[1:]]==['lazy','lazy']
+ assert len(soup.select('[data-sf-slide-step]'))==2
+ assert soup.select_one('.sf-hero-count').text=='1 / 3'
+ assert [x.text for x in soup.select('.sf-hero-copy h1')]==['A']
+ assert [x.text for x in soup.select('.sf-hero-copy h2')]==['B','C']
+
+
+def test_slides_are_opt_in_legacy_markup_unchanged():
+ old={'id':'legacy-banner-01','type':'hero','title':'Original','description':'Intro','image':'/media/x.jpg'}
+ html=render_blocks([old],{}, {'route':'/'},[])
+ assert 'sf-hero-slider' not in html and 'Original' in html
+ with_slide=new_block('hero',{'slides':slides('A')})
+ assert len(BeautifulSoup(render_blocks([with_slide],{}, {'route':'/about/'},[]),'html.parser').select('.sf-hero-panel'))==1
+ assert 'data-sf-slide-step' not in render_blocks([with_slide],{}, {'route':'/about/'},[])
+
+@pytest.mark.parametrize('bad',[
+ [dict(title='',image='javascript:alert(1)',href='javascript:alert(1)',button='hi ')]})
+ html=render_blocks([block],{}, {'route':'/'},[])
+ assert ''}]
+ manual=new_block('attributes',{'source_mode':'custom','items':added})
+ custom=render_blocks([manual],{},page,[])
+ assert '车长' in custom and 'BYD' not in custom
+ assert ''),
+ ('hero','image_fit','calc(100vw)'),
+ ('collection','card_style','custom'),
+ ('collection','image_fit','url(javascript:1)'),
+])
+def test_invalid_presentation_options_blocked(typ,key,value):
+ with pytest.raises(ValueError,match='不支持的区块展示选项'):
+ blueprints.validate_blocks([blueprints.new_block(typ,{key:value})])
+
+
+def test_legacy_blocks_preserve_default_appearance():
+ old_hero={'id':'hero-legacy-123','type':'hero','title':'旧首页','image':''}
+ old_list={'id':'cards-legacy-123','type':'collection','title':'旧列表','source':'product','limit':6,'columns':3}
+ markup=blueprints.render_blocks([old_hero,old_list],{}, {'route':'/','type':'home'},[])
+ assert 'sf-hero-layout-split' not in markup
+ assert 'sf-card-style-standard' not in markup
+ assert 'sf-fit-cover' not in markup
+ assert 'sf-section sf-collection' in markup
+
+
+def test_studio_uses_plain_language_and_shared_presentation():
+ script=(ROOT/'app/static/editor.js').read_text()
+ stylesheet=(ROOT/'app/static/blocks.css').read_text()
+ for phrase in ('Banner 排版','文字左侧 · 图片右侧','图片左侧 · 文字右侧','图片优先 · 大图展示','完整显示(不裁切)'):
+ assert phrase in script
+ assert 'sf-hero-layout-image-first' in stylesheet
+ assert 'sf-card-style-showcase' in stylesheet
+ assert 'sf-fit-contain .sf-item-img img' in stylesheet
+
+
+def test_real_preview_and_build_use_same_variants(factory):
+ sid=site(factory)
+ pages=factory.get(f'/api/sites/{sid}/entries').json()
+ home=next(x for x in pages if x['route']=='/')
+ blocks=[blueprints.new_block('hero',{'title':'产品供应','layout_variant':'image-first','image_fit':'contain'}),
+ blueprints.new_block('collection',{'title':'最新产品','card_style':'showcase','image_fit':'contain'})]
+ saved=factory.put(f'/api/sites/{sid}/pages/{home["id"]}/blueprint',json={'blocks':blocks,'page_template':'home'})
+ assert saved.status_code==200,saved.text
+ built=factory.post(f'/api/sites/{sid}/builds',json={})
+ assert built.status_code==200,built.text
+ version=built.json()['version']
+ built_html=factory.get(f'/preview/{sid}/{version}/').text
+ assert 'sf-hero-layout-image-first sf-fit-contain' in built_html
+ assert 'sf-card-style-showcase sf-fit-contain' in built_html
+ css=factory.get(f'/preview/{sid}/{version}/assets/blocks.css')
+ assert css.status_code==200
+ assert 'sf-card-style-showcase' in css.text
+ assert not factory.get(f'/api/sites/{sid}/builds/{version}/report').json()['errors']
diff --git a/themes/trade-pro/pages/brand.html b/themes/trade-pro/pages/brand.html
index d4105cc..9705456 100644
--- a/themes/trade-pro/pages/brand.html
+++ b/themes/trade-pro/pages/brand.html
@@ -1,85 +1 @@
-{# trade-pro brand detail — 品牌详情精修(Task 11,次要页面收官)。
- 设计参照(本轮无 cms 源对应):上游 themes/automotive/versions/1.4.0/
- pages/brand.html 的信息架构——品牌横幅(面包屑 + 眉题/h1/导语 + 页内
- 锚点按钮 + 品牌图)→ 车型/产品网格(section heading + "全部"动作位)→
- 品牌故事(眉题 + h2 + 正文);industrial 1.3.0 为同构薄形态。本模板以
- sf-tp 设计语言重组:sf-tp-page-hero(10c 内页 hero:封面底图 + brand-soft
- 罩层)+ sf-tp-section-head/card-grid(10a 卡片系)+ sf-tp-article-body
- 排版层双挂(10d)。
- 数据事实(make_context 对 brand kind):related = fields.brand 与页题
- (或 fields.brand 字段)一致的产品/车型集合——品牌页的产品网格只信它;
- 派生品牌页(builder 从产品 brand 字段派生 /brands//)body/cover
- 恒空、summary 是平台占位串「相关品牌及产品」——导语按 authored 判定
- 排除占位串(home.html 排除「首页」同款决策),空品牌不虚构介绍:
- 无产品给数据无关的英文导语 + 空态卡,绝不编造品牌故事。
- 面包屑 Brands 链接 /brands/(required_sections 恒保证该路由存在);
- hero 锚点按钮锚到 #tp-brand-products(构建器豁免 # 锚点,10h 同款)。 #}
-{% set tp_brand_summary = page.summary if page.summary and page.summary != '相关品牌及产品' else '' %}
-{# 导语优先级:authored 摘要 > 数据派生计数(Jinja 三元与 or 的优先级易误读,
- 显式分行赋值避免歧义)> 空品牌通用引导 #}
-{% if tp_brand_summary %}{% set tp_brand_lead = tp_brand_summary %}
-{% elif related %}{% set tp_brand_lead = ('One product from ' if related|length == 1 else related|length ~ ' products from ') ~ page.title ~ ' — specifications and export documentation included.' %}
-{% else %}{% set tp_brand_lead = 'Products assigned to this brand in the admin console appear here automatically.' %}{% endif %}
-
- {% if safe_image(page.cover) %} {% endif %}
-
-
-
-{% if related %}
-
-{% else %}
-
-{% endif %}
-{% if content %}
-
-
-
-
- BRAND PROFILE
-
About {{ page.title }}
-
-
-
{{ body_copy(content, page) }}
-
-
-{% endif %}
+BRAND PROFILE / PARTNERS {{page.title}} {{page.summary}}
{{cover(page,'brand')}}
{{body_copy(content,page)}}{% if related %}{{section_title('PRODUCT LINE','Related products','/products/')}}{{grid(related,'product')}}{% endif %}
diff --git a/themes/trade-pro/pages/faq.html b/themes/trade-pro/pages/faq.html
index c8e6db5..6db86e9 100644
--- a/themes/trade-pro/pages/faq.html
+++ b/themes/trade-pro/pages/faq.html
@@ -1,56 +1,2 @@
-{# trade-pro faq — FAQ 页精修(Task 11)。
- 设计参照(本轮无 cms 源对应):上游 industrial/automotive 1.3.0/1.4.0 的
- faq 页与 service 共用通用信息页薄形态;本模板以 sf-tp 设计语言定 FAQ
- 专属口径:sf-tp-page-hero(10c)→ 问题正文(sf-tp-article-body 排版层
- 双挂,10d)→ 深色引导联系卡(找不到答案 → /contact/,10d 侧栏 CTA
- 同款深底白按钮语汇)。
- 数据事实(make_context 对 faq kind):平台无 faq 内容类型——FAQ 页由
- 普通页面条目承担(meta.page_template='faq' 显式指定,或 /faq 前缀路由
- 推断),内容 = 单一富文本 body。sanitize 的 ALLOWED_TAGS 不含
- details/summary(正文富文本带不进折叠结构);theme.css 的 details/
- summary 折叠样式(Task 8)服务的是块系统注入的 custom_blocks(编辑器
- faq 块原生 details 渲染在 sf-canvas 区),与本模板正交共存——模板
- 正文只能走排版层,问题/答案的分节由正文自身 h2/h3 承担(10g/10h 同
- 决策)。body 为空 → 正文区块整段隐藏换空态卡;引导联系卡恒渲染
- (真实动作位)。面包屑/链接指向 /faq/(required_sections 恒保证;
- 当前页恰为 /faq/ 时自指隐藏)。 #}
-
- {% if safe_image(page.cover) %} {% endif %}
-
-
-
- Home {% if route != '/faq/' %}FAQ {% endif %}{{ page.title }}
-
-
-
SUPPORT / FAQ
-
{{ page.title }}
- {% if page.summary %}
{{ page.summary }}
{% endif %}
-
-
-
-
-
- {% if content %}
-
-
- ANSWERS
-
Frequently asked questions
-
-
-
{{ body_copy(content, page) }}
- {% else %}
-
-
No answers published here yet
-
Questions added to this page in the admin console appear here automatically.
-
- {% endif %}
-
-
-
+SUPPORT / FAQ {{page.title}} {{page.summary}}
+ANSWERS Frequently asked questions. Cooperation process, trade terms and common questions.
{{body_copy(content,page)}}{% if page.cover %}
{% endif %}
diff --git a/themes/trade-pro/pages/notfound.html b/themes/trade-pro/pages/notfound.html
index f07242b..cf49837 100644
--- a/themes/trade-pro/pages/notfound.html
+++ b/themes/trade-pro/pages/notfound.html
@@ -1,22 +1 @@
-{# trade-pro notfound — 品牌化 404(Task 11)。
- 设计参照(本轮无 cms 源对应):上游 industrial/automotive 1.3.0/1.4.0
- 的 404 为「眉题 + 巨号 404 + 一句文案 + 返回首页」的居中薄形态;本模板
- 保留该信息架构并以 sf-tp 深色带语汇品牌化(10a 应用带的藏青渐变 + 光晕
- 底、反色文案、白底按钮反色),双动作位:返回首页 + 浏览产品目录。
- 渲染流:构建器以 mode='notfound' 渲染(noindex meta),pro.html 对
- notfound 不渲染结尾 CTA 带;h1 唯一(「404」大标题即页题)。图标走
- Task 9 icon 宏(search:找不到页面的语义),无裸箭头字符。 #}
-
-
-
-
{{ icon('search') }}
-
ERROR / 404
-
404
-
The page you are looking for does not exist or has been moved.
-
-
-
-
+
diff --git a/themes/trade-pro/pages/search.html b/themes/trade-pro/pages/search.html
index db8f944..56a6217 100644
--- a/themes/trade-pro/pages/search.html
+++ b/themes/trade-pro/pages/search.html
@@ -1,31 +1,2 @@
-{# trade-pro search — 站内搜索页精修(Task 11)。
- 机制决策:采用与上游四主题完全相同的平台静态检索机制(V8.31 起全主题
- 共享,非 automotive 专属)——构建器为每个构建打包 search-index.json
- (全站条目标题/摘要/类型/品牌),site-runtime.js 在客户端完成 ?q= 深链
- 读取 + 关键词过滤 + 结果渲染,无任何服务端 API。JS 寻址的是 ID 钩子
- (#sf-search 输入 / #sf-search-results 容器 / #sf-search-count 计数,
- factory.css 的 .sf-search-page 提供基线观感)——三钩子与 .sf-search-page
- 类名必须原样保留(平台契约),否则检索退化为一页死输入框。
- 本模板的精修是观感层:hero 换 sf-tp-page-hero(10c 内页 hero 同款浅
- 品牌横幅,搜索是工具页无层级——不带面包屑,与上游一致),搜索面板由
- pro.css 的 Task 11 作用域规则把 factory.css 的 system-ui 基线覆写为
- cms 令牌观感(含 site-runtime.js 注入的 .sf-search-result 结果卡)。
- 构建器生成本页时 kind='search'(route 推断先于显式模板),robots 为
- noindex,follow;pro.html 对 search 页不渲染结尾 CTA 带。页面为英文
- (构建器的中文页题「站内搜索」仅进 ,不进正文)。 #}
-
-
-
-
-
SEARCH / CONTENT
-
Site Search
-
Search products, brands, news and cases across this site.
-
-
-
-
- Enter a keyword
-
-
-
-
+SEARCH / CONTENT Site Search Search products, brands, news and cases across this site.
+Enter a keyword
diff --git a/themes/trade-pro/pages/service.html b/themes/trade-pro/pages/service.html
index e4af976..2f57ad8 100644
--- a/themes/trade-pro/pages/service.html
+++ b/themes/trade-pro/pages/service.html
@@ -1,47 +1,2 @@
-{# trade-pro service detail — 服务详情精修(Task 11)。
- 设计参照(本轮无 cms 源对应):上游 industrial/automotive 1.3.0/1.4.0 的
- service 页为通用信息页薄形态(hero + 双栏正文 + 封面图);本模板以
- sf-tp 设计语言重组为「详情页」口径:sf-tp-page-hero(10c:封面底图 +
- 面包屑)→ detail 双栏(10g 案例页同构 1.2fr/0.8fr:文案列 = 眉题 + h2 +
- 正文排版层;侧列 = sticky CTA 卡 + 返回服务列表)。
- 数据事实(make_context 对 service kind):仅页面条目上下文(title/
- summary/body/cover),无服务专属集合;正文 = 单一富文本 body,小节结构
- 由正文自身 h2 承担(10g/10h 同决策),sf-tp-article-body 排版层双挂
- (10d)。body 为空 → 正文区块整段隐藏,双栏退化为居中单列(CTA 卡
- 保留——真实动作位,非虚构内容)。
- 面包屑/返回链接指向 /services/(required_sections 恒保证该路由存在;
- 当前页恰为 /services/ 时自指链接隐藏,面包屑降级为 Home › 标题)。 #}
-
- {% if safe_image(page.cover) %} {% endif %}
-
-
-
- Home {% if route != '/services/' %}Services {% endif %}{{ page.title }}
-
-
-
SERVICE / SUPPORT
-
{{ page.title }}
- {% if page.summary %}
{{ page.summary }}
{% endif %}
-
-
-
-
-
- {% if content %}
-
-
SERVICE DETAIL
-
{{ page.title }}
-
{{ body_copy(content, page) }}
-
- {% endif %}
-
-
-
+OUR SERVICES {{page.title}} {{page.summary}}
+SERVICE / SUPPORT {{page.title}} Working process, service scope and support commitments.
{{body_copy(content,page)}}{% if page.cover %}
{% endif %}
diff --git a/themes/trade-pro/pro.css b/themes/trade-pro/pro.css
index 805f8ea..f1b37bf 100644
--- a/themes/trade-pro/pro.css
+++ b/themes/trade-pro/pro.css
@@ -745,85 +745,3 @@ body.sf-theme-trade-pro{overflow-x:hidden;overflow-x:clip}
.sf-theme-trade-pro .pro-footer-top{grid-template-columns:1fr;gap:var(--sf-tp-space-8)}
.sf-theme-trade-pro .pro-footer-bottom{display:block}
}
-
-/* ── Task 11:brand/service/faq/search/notfound 次要页面精修 ──
- 设计参照:上游四主题 1.3.0/1.4.0 对应页的信息架构(automotive 1.4.0
- brand 页最完整:横幅 + 面包屑 + 锚点按钮 + 车型网格 + 品牌故事;其余为
- 通用信息页骨架)+ 已建立的 sf-tp 设计语言。共享组件零重复声明:
- brand 的 hero/卡片网格/正文分别由 10c page-hero、10a card-grid、
- 10d article-body 既有规则(含 10a/10c 的 900/767/620 折叠块)覆盖——
- brand 模板无新增视觉规则;本块只补 service 双栏/CTA 卡、faq 引导卡、
- search 面板与结果卡、notfound 深色带。级联:全部为 Task 11 新类名的
- 首次声明,位于 10a-10j 既有块之后,无同特异性竞争;search 段按
- .sf-theme-trade-pro 作用域(0,2,0+)覆写 factory.css 的 .sf-search-page
- 基线(0,1,0)与 site-runtime.js 注入的 .sf-search-result,机制钩子
- (ID 与类名)不在样式层触碰。 */
-
-/* service 双栏(10g 案例页 1.2fr/0.8fr 同构):文案列 + sticky CTA 侧列;
- 正文为空时 --solo 单列,CTA 卡居中收窄 */
-.sf-theme-trade-pro .sf-tp-service-grid{display:grid;grid-template-columns:1.2fr 0.8fr;gap:calc(var(--sf-tp-space-12) + var(--sf-tp-space-4));align-items:start}
-.sf-theme-trade-pro .sf-tp-service-grid--solo{grid-template-columns:minmax(0,1fr)}
-.sf-theme-trade-pro .sf-tp-service-grid--solo .sf-tp-service-aside{width:100%;max-width:560px;margin-inline:auto}
-.sf-theme-trade-pro .sf-tp-service-copy h2{margin-block:var(--sf-tp-space-2) var(--sf-tp-space-6)}
-.sf-theme-trade-pro .sf-tp-service-aside{position:sticky;top:calc(var(--sf-tp-space-16) + var(--sf-tp-space-4));display:grid;gap:var(--sf-tp-space-4);justify-items:start}
-.sf-theme-trade-pro .sf-tp-service-cta{display:grid;gap:var(--sf-tp-space-3);padding:var(--sf-tp-card-padding-md);background:var(--sf-tp-color-surface-muted);border-radius:var(--sf-tp-card-border-radius)}
-.sf-theme-trade-pro .sf-tp-service-cta h3{margin:0;font-size:var(--sf-tp-font-size-xl)}
-.sf-theme-trade-pro .sf-tp-service-cta p{margin:0;color:var(--sf-tp-color-text-muted);font-size:var(--sf-tp-font-size-sm)}
-.sf-theme-trade-pro .sf-tp-service-cta .sf-tp-btn{justify-self:start}
-.sf-theme-trade-pro .sf-tp-service-back{display:inline-flex;align-items:center;gap:var(--sf-tp-space-2);font-size:var(--sf-tp-font-size-sm);font-weight:var(--sf-tp-font-weight-semibold);color:var(--sf-tp-color-text-link)}
-.sf-theme-trade-pro .sf-tp-service-back:hover{color:var(--sf-tp-color-text-link-hover)}
-
-/* faq 引导联系卡(10d 侧栏 CTA 深底白按钮语汇):正文之后横排收束 */
-.sf-theme-trade-pro .sf-tp-faq-guide{display:flex;align-items:center;justify-content:space-between;gap:var(--sf-tp-space-6);flex-wrap:wrap;margin-block-start:var(--sf-tp-space-10);padding:var(--sf-tp-card-padding-md);background:var(--sf-tp-color-surface-inverse);border-radius:var(--sf-tp-card-border-radius);color:var(--sf-tp-color-text-inverse)}
-.sf-theme-trade-pro .sf-tp-faq-guide .eyebrow{color:var(--sf-tp-color-text-inverse-muted)}
-.sf-theme-trade-pro .sf-tp-faq-guide h3{margin-block-end:var(--sf-tp-space-2);color:var(--sf-tp-color-text-inverse)}
-.sf-theme-trade-pro .sf-tp-faq-guide p{margin:0;color:color-mix(in srgb,var(--sf-tp-color-text-inverse) 82%,transparent);font-size:var(--sf-tp-font-size-sm)}
-.sf-theme-trade-pro .sf-tp-faq-guide .sf-tp-btn{background:var(--sf-tp-color-surface-default);border-color:var(--sf-tp-color-surface-default);color:var(--sf-tp-color-text-brand)}
-.sf-theme-trade-pro .sf-tp-faq-guide .sf-tp-btn:hover{background:var(--sf-tp-color-brand-primary);border-color:var(--sf-tp-color-brand-primary);color:var(--sf-tp-color-text-on-brand)}
-/* 空态卡内的按钮(brand 空品牌/browse 产品):与文案拉开间距 */
-.sf-theme-trade-pro .sf-tp-listing-empty .sf-tp-btn{margin-block-start:var(--sf-tp-space-4)}
-
-/* search 面板:factory.css 的 .sf-search-page system-ui 基线 → cms 令牌
- 观感(含 site-runtime.js 注入的 .sf-search-result 结果卡样式);
- #sf-search/#sf-search-results/#sf-search-count 三钩子为平台 JS 契约 */
-.sf-theme-trade-pro .sf-search-page{min-height:auto;width:min(880px,calc(100% - 32px));margin-inline:auto;padding-block:calc(var(--sf-tp-space-10) + var(--sf-tp-space-4)) calc(var(--sf-tp-space-16) + var(--sf-tp-space-8));font-family:var(--sf-tp-font-family-sans);color:var(--sf-tp-color-text-primary)}
-.sf-theme-trade-pro .sf-search-page>label{display:block;margin:0 0 var(--sf-tp-space-3);font-size:var(--sf-tp-font-size-xs);font-weight:var(--sf-tp-font-weight-semibold);letter-spacing:var(--sf-tp-letter-spacing-wide);text-transform:uppercase;color:var(--sf-tp-color-text-muted)}
-.sf-theme-trade-pro .sf-search-page input[type="search"]{display:block;width:100%;padding:var(--sf-tp-space-4) calc(var(--sf-tp-space-4) + var(--sf-tp-space-1));font-size:var(--sf-tp-font-size-lg);font-family:inherit;border:var(--sf-tp-border-width-hairline) solid var(--sf-tp-color-border-default);border-radius:var(--sf-tp-radius-sm);background:var(--sf-tp-color-surface-default);color:var(--sf-tp-color-text-primary)}
-.sf-theme-trade-pro .sf-search-page #sf-search-count{margin:var(--sf-tp-space-5) 0 var(--sf-tp-space-3);color:var(--sf-tp-color-text-muted);font-size:var(--sf-tp-font-size-sm)}
-.sf-theme-trade-pro .sf-search-result{padding:var(--sf-tp-space-5) 0;border-block-end:var(--sf-tp-border-width-hairline) solid var(--sf-tp-color-border-subtle)}
-.sf-theme-trade-pro .sf-search-result a{color:var(--sf-tp-color-text-primary);font-size:var(--sf-tp-font-size-xl);font-weight:var(--sf-tp-font-weight-bold);text-decoration:none}
-.sf-theme-trade-pro .sf-search-result a:hover{color:var(--sf-tp-color-text-brand)}
-.sf-theme-trade-pro .sf-search-result small{display:block;margin-block-start:var(--sf-tp-space-1);color:var(--sf-tp-color-text-brand);font-size:var(--sf-tp-font-size-xs);letter-spacing:var(--sf-tp-letter-spacing-wide);text-transform:uppercase}
-.sf-theme-trade-pro .sf-search-result p{margin-block-start:var(--sf-tp-space-2);color:var(--sf-tp-color-text-muted);font-size:var(--sf-tp-font-size-sm)}
-
-/* notfound 深色带(10a 应用带语汇:藏青渐变 + 光晕 + 反色文案):
- 居中单列,巨号 404 为唯一 h1;双按钮(实底 + 深底上的白底反色 ghost) */
-.sf-theme-trade-pro .sf-tp-notfound{position:relative;overflow:hidden;padding-block:calc(var(--sf-tp-space-16) + var(--sf-tp-space-8)) calc(var(--sf-tp-space-16) + var(--sf-tp-space-4));color:var(--sf-tp-color-text-inverse);background:linear-gradient(115deg,var(--sf-tp-color-surface-inverse),var(--sf-tp-color-brand-primary-deep) 78%)}
-.sf-theme-trade-pro .sf-tp-notfound::before{content:"";position:absolute;inset:0;background:radial-gradient(circle at 15% 20%,color-mix(in srgb,var(--sf-tp-color-brand-primary) 24%,transparent),transparent 42%);pointer-events:none}
-.sf-theme-trade-pro .sf-tp-notfound .wrap{position:relative;z-index:var(--sf-tp-z-raised)}
-.sf-theme-trade-pro .sf-tp-notfound-inner{display:grid;justify-items:center;max-width:560px;margin-inline:auto;text-align:center}
-.sf-theme-trade-pro .sf-tp-notfound-inner .eyebrow{color:var(--sf-tp-color-text-inverse-muted)}
-.sf-theme-trade-pro .sf-tp-notfound-inner h1{margin-block:var(--sf-tp-space-2) var(--sf-tp-space-3);font-size:clamp(120px,22vw,220px);line-height:.9;letter-spacing:var(--sf-tp-letter-spacing-tight);color:var(--sf-tp-color-text-inverse)}
-.sf-theme-trade-pro .sf-tp-notfound-icon{background:color-mix(in srgb,var(--sf-tp-color-neutral-0) 12%,transparent);color:var(--sf-tp-color-text-inverse)}
-.sf-theme-trade-pro .sf-tp-notfound-lead{color:color-mix(in srgb,var(--sf-tp-color-text-inverse) 88%,transparent);font-size:var(--sf-tp-font-size-lg);margin-block-end:var(--sf-tp-space-8)}
-.sf-theme-trade-pro .sf-tp-notfound-actions{display:flex;gap:var(--sf-tp-space-4);flex-wrap:wrap;justify-content:center}
-.sf-theme-trade-pro .sf-tp-notfound .sf-tp-btn--ghost{background:var(--sf-tp-color-surface-default);border-color:var(--sf-tp-color-surface-default);color:var(--sf-tp-color-text-brand)}
-
-/* ── Task 11 响应式(新类名折叠收本块;hero 竖版/卡片栅格/正文紧凑由
- 10c/10a/10d 既有媒体块经共用类名覆盖——10c 教训:同特异性按源顺序
- 取胜,本块只声明 Task 11 新类名)── */
-@media (max-width: 900px){
- .sf-theme-trade-pro .sf-tp-service-grid{grid-template-columns:1fr;gap:var(--sf-tp-space-8)}
- .sf-theme-trade-pro .sf-tp-service-aside{position:static}
-}
-@media (max-width: 620px){
- .sf-theme-trade-pro .sf-tp-service-cta{padding:16px}
- .sf-theme-trade-pro .sf-tp-faq-guide{display:block}
- .sf-theme-trade-pro .sf-tp-faq-guide .sf-tp-btn{margin-block-start:12px}
- .sf-theme-trade-pro .sf-search-page{padding-block:28px 72px}
- .sf-theme-trade-pro .sf-search-page input[type="search"]{padding:13px 16px;font-size:var(--sf-tp-font-size-md)}
- .sf-theme-trade-pro .sf-search-result a{font-size:var(--sf-tp-font-size-lg)}
- .sf-theme-trade-pro .sf-tp-notfound{padding-block:64px 56px}
- .sf-theme-trade-pro .sf-tp-notfound-inner h1{font-size:clamp(96px,26vw,140px)}
- .sf-theme-trade-pro .sf-tp-notfound-actions{display:grid;grid-template-columns:1fr;justify-items:stretch}
-}