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':'

在这里输入图文内容。

'}}, 'features':{'name':'核心优势','icon':'▦','fields':['title','items'],'defaults':{'title':'我们的优势','items':[{'title':'专业服务','text':'从咨询到交付'},{'title':'品质保障','text':'严格的质量流程'},{'title':'全球网络','text':'连接不同市场'}]}}, 'stats':{'name':'数字指标','icon':'#','fields':['title','items'],'defaults':{'title':'我们的实力','items':[{'value':'50,000+','label':'服务案例'},{'value':'70+','label':'市场覆盖'},{'value':'30+','label':'合作伙伴'}]}}, - 'collection':{'name':'动态内容列表','icon':'▣','fields':['title','subtitle','source','limit','columns','button','href'],'defaults':{'title':'精选产品','subtitle':'为客户提供适合的解决方案','source':'product','limit':6,'columns':3,'button':'查看全部','href':'/product/'}}, + 'collection':{'name':'动态内容列表','icon':'▣','fields':['title','subtitle','source','limit','columns','button','href','card_style','image_fit'],'defaults':{'title':'精选产品','subtitle':'为客户提供适合的解决方案','source':'product','limit':6,'columns':3,'button':'查看全部','href':'/product/','card_style':'standard','image_fit':'contain'}}, 'image_text':{'name':'图文组合','icon':'◧','fields':['title','subtitle','description','image','image_side','button','href'],'defaults':{'title':'从采购到交付','subtitle':'OUR APPROACH','description':'展示业务流程和服务理念。','image':'','image_side':'right','button':'了解更多','href':'/about/'}}, 'gallery':{'name':'图片画廊','icon':'▧','fields':['title','images','columns'],'defaults':{'title':'精彩瞬间','images':[],'columns':3}}, 'faq':{'name':'常见问题','icon':'?','fields':['title','items'],'defaults':{'title':'常见问题','items':[{'question':'如何开始合作?','answer':'您可以通过联系我们页面提交需求。'}]}}, 'cta':{'name':'行动号召','icon':'↗','fields':['title','subtitle','button','href'],'defaults':{'title':'开启新的合作','subtitle':'告诉我们您的需求,我们将提供适合的方案。','button':'联系我们','href':'/contact/'}}, - 'attributes':{'name':'参数信息表','icon':'▤','fields':['title'],'defaults':{'title':'产品参数'}}, + 'attributes':{'name':'参数信息表','icon':'▤','fields':['title','source_mode','items'],'defaults':{'title':'产品参数','source_mode':'page','items':[]}}, 'layout':{'name':'多列自由布局','icon':'▥','fields':['columns','gap','tablet_columns','mobile_columns'], 'defaults':{'columns':[{'width':1,'blocks':[]},{'width':1,'blocks':[]}],'gap':24,'tablet_columns':2,'mobile_columns':1}}, 'spacer':{'name':'上下留白','icon':'↕','fields':['height'],'defaults':{'height':48}}, 'carousel':{'name':'图片轮播','icon':'▧','fields':['title','items'],'defaults':{'title':'精选展示','items':[{'image':'','alt':'展示图片','caption':'图片说明'}]}}, @@ -32,7 +32,7 @@ BLOCKS = { # are not supported; all values are still verified by validate_blocks. BLOCK_PROPERTY_SCHEMA={key:{ 'type':'object','additionalProperties':False, - 'properties':{field:{'type':('array' if field in ('items','images','columns') else + 'properties':{field:{'type':('array' if field in ('items','images','columns','slides') else 'integer' if field in ('height','limit','gap','tablet_columns','mobile_columns') else 'string')} for field in spec['fields']}, 'supported_devices':['desktop','tablet','mobile'], @@ -104,6 +104,35 @@ def validate_blocks(blocks): if not re.fullmatch('[a-zA-Z0-9_-]{4,50}',ident) or ident in found:raise ValueError('区块 ID 缺失或重复') found.add(ident) spec=BLOCKS[block['type']] + visual_options={ + 'hero':{'layout_variant':('split','image-first','centered'),'image_fit':('cover','contain')}, + 'collection':{'card_style':('standard','compact','showcase'),'image_fit':('cover','contain')}, + } + for visual_key, choices in visual_options.get(block['type'],{}).items(): + if visual_key in block and (not isinstance(block[visual_key],str) or block[visual_key] not in choices): + raise ValueError('不支持的区块展示选项: '+visual_key) + # V8.34.1: strict item schemas. Never trust arbitrary HTML/URLs in slides. + if block['type']=='hero' and 'slides' in block: + slides=block['slides'] + if not isinstance(slides,list) or len(slides)>6: + raise ValueError('首页横幅最多支持6张') + fields=('title','subtitle','description','image','button','href') + for slide in slides: + if not isinstance(slide,dict) or set(slide)-set(fields): + raise ValueError('横幅内容字段无效') + if any(not isinstance(v,str) or len(v)>2000 for v in slide.values()): + raise ValueError('横幅文字或图片地址无效') + if block['type']=='attributes': + mode=block.get('source_mode','page') + if not isinstance(mode,str) or mode not in ('page','custom','merge'): + raise ValueError('参数来源方式无效') + items=block.get('items',[]) + if not isinstance(items,list) or len(items)>30: + raise ValueError('参数最多支持30项') + for item in items: + if (not isinstance(item,dict) or set(item)!={'label','value'} or + any(not isinstance(item[k],str) or len(item[k])>300 for k in ('label','value'))): + raise ValueError('参数项必须包含名称和数值') allowed=set(spec['fields'])|{'id','type','visible','style','shared_ref'} normalized={key:val for key,val in block.items() if key in allowed} normalized['id']=ident @@ -153,6 +182,7 @@ def validate_blocks(blocks): normalized['gap']=max(0,min(64,int(block.get('gap',24)))) normalized['tablet_columns']=tablet_cols normalized['mobile_columns']=mobile_cols + if block['type']=='hero' and 'slides' in block: normalized['slides']=[dict(x) for x in block['slides']] if block['type'] in ('carousel','tabs','pricing'): items=block.get('items',[]) if not isinstance(items,list) or len(items)>16:raise ValueError('轮播、标签和方案最多16条') @@ -245,9 +275,30 @@ def render_blocks(blocks, site, page, entries): start=f'
' end='
' if typ=='hero': + slides=block.get('slides') or [] + if slides: + panels=[] + for idx,slide in enumerate(slides[:6]): + # Text binding is resolved per slide; outputs are escaped at HTML boundaries. + st=escape(resolve_text(slide.get('title',''),site,page)) + ss=escape(resolve_text(slide.get('subtitle',''),site,page)) + sd=escape(resolve_text(slide.get('description',''),site,page)) + si=safe_media(resolve_text(slide.get('image',''),site,page)) + button=escape(resolve_text(slide.get('button',''),site,page) or '查看详情') + link=safe_href(resolve_text(slide.get('href','/product/'),site,page)) + art=(f'
{st}
' if si else '') + heading='h1' if idx==0 else 'h2' # one primary page heading across all slides + panels.append(f'
{ss}<{heading}>{st}

{sd}

{button}
{art}
') + controls=('
1 / '+str(len(slides))+'
' if len(slides)>1 else '') + parts.append(start+f'
{"".join(panels)}
{controls}
'+end) + continue image=safe_media(resolve('image')) - art=f'
{title}
' if image else '
✧
' - parts.append(start+'
'+subtitle+'

'+title+'

'+desc+'

'+escape(resolve('button','探索更多'))+' ↗
'+art+'
'+end) + art=f'
{title}
' if image else '
✧
' + variant=block.get('layout_variant','split') + fit=block.get('image_fit','cover') + visual_cls=(f'sf-hero-layout-{variant} sf-fit-{fit}' if 'layout_variant' in block or 'image_fit' in block else '') + legacy_action='↗' if not visual_cls else '' + parts.append(start+f'
'+subtitle+'

'+title+'

'+desc+'

'+escape(resolve('button','探索更多'))+legacy_action+'
'+art+'
'+end) elif typ=='richtext': body=sanitize(resolve('body')) parts.append(start+f'

{title}

{body}
'+end) @@ -274,7 +325,10 @@ def render_blocks(blocks, site, page, entries): # Homepage highlights are not catalogues: avoid injecting search/filters before featured cards. # Other listing pages may opt into filters for sufficiently large result sets. filterable=(page.get('route')!='/' and (page.get('type') in ('category','brand') or count>=9)) - parts.append(start+f'
{subtitle}

{title}

{escape(resolve("button","查看全部"))}
{cards if cards else "

当前暂无相关内容,请导入或添加数据。

"}
'+end) + card_style=block.get('card_style','standard') + image_fit=block.get('image_fit','contain') + card_classes=(' sf-card-style-'+card_style if 'card_style' in block else '')+(' sf-fit-'+image_fit if 'image_fit' in block else '') + parts.append(start+f'
{subtitle}

{title}

{escape(resolve("button","查看全部"))}
{cards if cards else "

当前暂无相关内容,请导入或添加数据。

"}
'+end) elif typ=='image_text': im=safe_media(resolve('image')); img=f'{title}' if im else '
✧
' side=' sf-reverse' if block.get('image_side')=='left' else '' @@ -288,7 +342,13 @@ def render_blocks(blocks, site, page, entries): candidates=[(field['name'],field.get('label') or field['name']) for field in specs] if not candidates: candidates=[(k,k.replace('_',' ').title()) for k in props.keys() if k not in ('blocks','page_template','wp_metadata','wp_post_id','wp_type','fields') and isinstance(props[k],(str,int,float,bool))] - pairs=''.join(f'
{escape(str(label))}{escape(str(props[key]))}
' for key,label in candidates if props.get(key) not in (None,'')) + source_mode=block.get('source_mode','page') + pairs='' + if source_mode in ('page','merge'): + pairs=''.join(f'
{escape(str(label))}{escape(str(props[key]))}
' for key,label in candidates if props.get(key) not in (None,'')) + if source_mode in ('custom','merge'): + manual=''.join(f'
{escape(resolve_text(item["label"],site,page))}{escape(resolve_text(item["value"],site,page))}
' for item in block.get('items',[])[:30] if item.get('label') and item.get('value')) + pairs+=manual parts.append(start+f'

{title}

{pairs if pairs else "

参数尚未填写

"}
'+end) elif typ=='gallery': pics=block.get('images') if isinstance(block.get('images'),list) else [] diff --git a/app/static/blocks.css b/app/static/blocks.css index 0f7dd27..cca3b6f 100644 --- a/app/static/blocks.css +++ b/app/static/blocks.css @@ -33,3 +33,39 @@ Restrict grid layout to its dedicated inner element. */ .sf-section.sf-features,.sf-section.sf-collection,.sf-section.sf-stats,.sf-section.sf-gallery,.sf-section.sf-attributes{display:block;grid-template-columns:none} .sf-section.sf-faq{max-width:none} + + +/* V8.34: opt-in no-code compositions; no change for legacy blocks with no choice. */ +.sf-hero-grid.sf-hero-layout-image-first > .sf-hero-img{order:-1} +.sf-hero-grid.sf-hero-layout-centered{grid-template-columns:minmax(0,1fr);text-align:center;justify-items:center} +.sf-hero-grid.sf-hero-layout-centered .sf-hero-copy{max-width:900px;margin-inline:auto} +.sf-hero-grid.sf-hero-layout-centered .sf-hero-img{width:100%;min-height:240px;max-height:540px} +.sf-hero-grid.sf-fit-contain .sf-hero-img img{object-fit:contain} +.sf-collection.sf-fit-contain .sf-item-img{background:#f4f7f9} +.sf-collection.sf-fit-contain .sf-item-img img{object-fit:contain} +.sf-collection.sf-card-style-compact .sf-item-img{height:156px} +.sf-collection.sf-card-style-compact .sf-item-copy{padding:16px 19px} +.sf-collection.sf-card-style-compact .sf-item-copy h3{font-size:clamp(16px,1.5vw,20px);margin:8px 0 5px} +.sf-collection.sf-card-style-compact .sf-item-copy p{font-size:13px;min-height:0} +.sf-collection.sf-card-style-showcase .sf-item-img{height:auto;aspect-ratio:16/10} +.sf-collection.sf-card-style-showcase .sf-item-copy{padding:25px} +.sf-collection.sf-card-style-showcase .sf-item-copy h3{font-size:clamp(19px,2vw,26px)} +@media(max-width:820px){.sf-hero-grid.sf-hero-layout-image-first > .sf-hero-img{order:initial}} +@media(max-width:540px){.sf-collection.sf-card-style-compact .sf-item-img{height:180px}.sf-hero-grid.sf-hero-layout-centered .sf-hero-img{min-height:200px}} +@media(prefers-reduced-motion:reduce){.sf-collection.sf-card-style-showcase .sf-item{transition:none}} + +/* Industrial product photography respects *explicit* image fit, not archived defaults. */ +.sf-theme-industrial .pro-custom-content .sf-collection.sf-fit-cover .sf-item-img img{object-fit:cover} +.sf-theme-industrial .pro-custom-content .sf-collection.sf-fit-contain .sf-item-img img{object-fit:contain} + + +/* V8.34.1 opt-in multi-Banner. No autoplay, low JS cost, native swipe and keyboard scroll. */ +.sf-hero-slider{min-width:0}.sf-hero-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain;scrollbar-width:none}.sf-hero-track::-webkit-scrollbar{display:none} +.sf-hero-panel{flex:0 0 100%;width:100%;min-width:0;scroll-snap-align:start;scroll-snap-stop:always} +.sf-hero-controls{display:flex;align-items:center;justify-content:center;gap:14px;margin-top:16px;color:inherit} +.sf-hero-controls button{border:1px solid currentColor;border-radius:50%;width:40px;height:40px;font-size:24px;line-height:1;background:transparent;color:inherit;cursor:pointer} +.sf-hero-controls button:focus-visible{outline:3px solid var(--accent,#3569d3);outline-offset:3px} +.sf-hero-count{font-size:13px;font-weight:700;min-width:40px;text-align:center} +@media(prefers-reduced-motion:reduce){.sf-hero-track{scroll-behavior:auto}} + +.sf-hero-panel .sf-hero-copy h2{font-size:clamp(38px,5vw,76px);font-weight:790;letter-spacing:-.052em} diff --git a/app/static/editor.css b/app/static/editor.css index 5b72af8..2e0ff30 100644 --- a/app/static/editor.css +++ b/app/static/editor.css @@ -171,3 +171,16 @@ body:not(.sf-advanced-mode) #tabStyle,body:not(.sf-advanced-mode) #tabResponsive @media(max-width:720px){.sf-steps .sf-guide-link{margin-left:auto}.sf-inspector-mode{padding:8px 11px}} .sf-photo-action strong{font-size:13px;color:#28516e}.sf-direct-upload{display:grid;gap:7px;color:#506b82;font-size:12px;font-weight:650}.sf-direct-upload input{font-size:11px;width:100%;max-width:100%} + +/* V8.34: visible, understandable no-code layout presets */ +.sf-visual-options{display:grid;grid-template-columns:1fr;gap:7px;margin-top:7px} +.sf-visual-option{display:flex;align-items:center;gap:12px;text-align:left;padding:10px 12px;background:#fff;border:1px solid #dce5ef;border-radius:9px;color:#354b62;font-size:12px;white-space:normal} +.sf-visual-option:hover{border-color:#8baee9;background:#f6f9ff} +.sf-visual-option.on{border-color:#366dc8;background:#eef5ff;color:#234d94;box-shadow:inset 3px 0 #366dc8} +.sf-option-mark{display:grid;place-items:center;flex:0 0 34px;width:34px;height:30px;border:1px solid #d9e5f6;border-radius:5px;background:#fff;color:#527bc1;font-size:20px} +.sf-visual-option:focus-visible{outline:3px solid #e0a43c;outline-offset:2px} + +/* V8.34.1: compact human-readable slide/spec editors. */ +.sf-slide-media{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:10px 0} +.sf-slide-media button,.sf-slide-media label{font-size:12px;border:1px solid #d6dee8;border-radius:7px;padding:8px 10px;cursor:pointer;background:#f9fbfd} +.sf-slide-media label input[type=file]{max-width:180px;display:block;margin-top:5px;font-size:11px} diff --git a/app/static/editor.js b/app/static/editor.js index 5ab0ec5..d11f2ea 100644 --- a/app/static/editor.js +++ b/app/static/editor.js @@ -347,16 +347,16 @@ const Studio={ fieldQuiet(key,value){const b=blocks[selected];if(!b)return;b[key]=value;Studio.modified(true);const row=$('#blockList').querySelector(`[data-index="${selected}"]`);if(row)row.querySelector('small').textContent=b.title||b.subtitle||b.type}, arrayEditor(field,value,type){ const rows=Array.isArray(value)?value:[]; - const labels={title:'标题',text:'文字介绍',value:'数字',label:'名称',question:'问题',answer:'回答',url:'图片地址',alt:'图片说明',image:'图片地址',caption:'图片说明',name:'名称',price:'价格',features:'方案包含的服务',href:'按钮跳转路径'}; - const spec=schema.blocks[type];const sample=(spec?.defaults?.[field]||[])[0]||{url:'',alt:''}; + const labels={title:'标题',text:'文字介绍',value:'数值',label:'参数名称',question:'问题',answer:'回答',url:'图片地址',alt:'图片说明',image:'图片地址',caption:'图片说明',name:'名称',price:'价格',features:'方案包含的服务',href:'按钮跳转路径',subtitle:'副标题',description:'说明文字',button:'按钮文字',image:'图片地址',value:'参数值'}; + const spec=schema.blocks[type];const sample=(spec?.defaults?.[field]||[])[0]||(field==='slides'?{title:'',subtitle:'',description:'',image:'',button:'',href:'/product/'}:type==='attributes'?{label:'',value:''}:{url:'',alt:''}); const keys=Object.keys(sample); return `

无需填写 JSON:按顺序编辑每一项,修改会自动显示在预览中。

`+ rows.map((row,i)=>`
第 ${i+1} 项
`+ - keys.map(key=>``).join('')+ - `
`).join('')+`
`; + keys.map(key=>``).join('')+ + `${field==='slides'?`
`:''}`).join('')+``; }, itemField(field,index,key,value){const block=blocks[selected],row=block?.[field]?.[index];if(!row)return;row[key]=key==='features'?String(value).split('\n').map(x=>x.trim()).filter(Boolean):String(value).slice(0,1500);Studio.modified(true)}, - addItem(field){const block=blocks[selected];if(!block)return;const rows=block[field];if(!Array.isArray(rows))return;const limit=field==='images'?24:16;if(rows.length>=limit)return toast(`最多添加 ${limit} 项`,true);const sample=schema.blocks[block.type]?.defaults?.[field]?.[0]||{url:'',alt:''};const next={};for(const [key,val] of Object.entries(sample))next[key]=Array.isArray(val)?[]:'';rows.push(next);Studio.modified();Studio.inspector()}, + addItem(field){const block=blocks[selected];if(!block)return;const rows=block[field];if(!Array.isArray(rows))return;const limit=field==='slides'?6:field==='images'?24:block.type==='attributes'?30:16;if(rows.length>=limit)return toast(`最多添加 ${limit} 项`,true);const sample=schema.blocks[block.type]?.defaults?.[field]?.[0]||(field==='slides'?{title:'',subtitle:'',description:'',image:'',button:'',href:'/product/'}:block.type==='attributes'?{label:'',value:''}:{url:'',alt:''});const next={};for(const [key,val] of Object.entries(sample))next[key]=Array.isArray(val)?[]:'';if(field==='slides'&&rows.length===0){for(const key of ['title','subtitle','description','image','button','href'])next[key]=String(block[key]||'')}rows.push(next);Studio.modified();Studio.inspector()}, removeItem(field,index){const rows=blocks[selected]?.[field];if(!Array.isArray(rows)||!rows[index])return;rows.splice(index,1);Studio.modified();Studio.inspector()}, moveItem(field,index,by){const rows=blocks[selected]?.[field],target=index+by;if(!Array.isArray(rows)||target<0||target>=rows.length)return;[rows[index],rows[target]]=[rows[target],rows[index]];Studio.modified();Studio.inspector()}, jsonField(key,value){try{let data=JSON.parse(value);if(!Array.isArray(data))throw Error('必须为数组');Studio.fieldQuiet(key,data);$('#jsonError').textContent='✓ JSON 格式有效';$('#jsonError').style.color='#238460'}catch(e){$('#jsonError').textContent='JSON 格式有误:'+e.message;$('#jsonError').style.color='#bf5a58'}}, @@ -370,10 +370,16 @@ const Studio={ toast('已应用区块外观,保存草稿前可以撤销'); }, inspector(){let target=$('#inspector');let b=blocks[selected];$('#blockTypeLabel').textContent=b?schema.blocks[b.type].name:'页面';if(!b){target.innerHTML='
还没有选中内容

先到左侧选择一个区块,或点击“添加内容区块”。

';return}const spec=schema.blocks[b.type];let out=`

${html(spec.name)}

`; - 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+=`
`; - 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+=`默认读取产品数据,不会更改原产品信息。`; else if(field==='source')out+=`网站已有内容将自动绑定并生成卡片。`; + 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])=>``).join('')}
`; + if(field==='image_fit')out+='汽车与管材产品建议选择“完整显示”,避免产品轮廓被裁切。'; + } else if(field==='image_side'||field==='align')out+=``; 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 · 真正生成的主题布局 + +
SiteForge V8.34 · Banner 与产品卡片来自实际服务端区块渲染 · 仅演示布局,不连接业务后台
+ \ 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 批次
JavaScript39/39 通过
HTTP + Worker登录、建站、构建、ZIP 下载、预览通过
Chromium 隔离预览36 组组合;0 JS 错误、0 横向溢出
安全输入非白名单视觉配置拒绝保存

实际渲染预览

V8.34 桌面端真实模块渲染截图

V8.34 手机端布局截图

可重现证据

生产限制

本报告不构成正式公网生产验收。未验证官方 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 + 人工参数补充;系统为开发集成版,不包含正式公网部署验收。

464Python 用例通过
40JavaScript 用例通过
20/20隔离批次通过
6浏览器设备宽度验证

实际渲染效果

电脑端 Banner 和参数表手机端 Banner 和参数表

已实现功能

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.pypassed5.55 s
test_console_v819.py, test_core_v4.py, test_deep_nested_v824.pypassed5.57 s
test_editor_easy_v8212.py, test_editor_ux_v8211.py, test_enterprise.pypassed4.26 s
test_enterprise_v7.py, test_factory.py, test_factory_v2.pypassed7.39 s
test_hesupipe_layout_v8331.py, test_mobile_tabs_v8213.py, test_multilingual_v815.pypassed6.67 s
test_nested_v823.py, test_p0_71.py, test_p0_backup.pypassed3.84 s
test_p0_p4_completion_v88.py, test_p0_p4_completion_v89.py, test_p1_platform.pypassed8.79 s
test_production_v816.py, test_professional_ui_v811.py, test_regression_runner_v817.pypassed5.32 s
test_release_native_v814.py, test_release_reviews_v7.py, test_release_v813.pypassed4.39 s
test_site_design_v87.py, test_siteforge_v817_audit.py, test_studio_v86.pypassed7.29 s
test_theme_pro_v3.py, test_theme_registry.py, test_v818_pg_recovery.pypassed13.83 s
test_v818_sqlite_pg_transfer.py, test_v821_editor_history_and_mapping.py, test_v822_canvas.pypassed4.02 s
test_v825_shared_components.py, test_v826_global_design.py, test_v827_guided_workspace.pypassed5.25 s
test_v828_launch_center.py, test_v829_theme_studio.py, test_v830_real_pages.pypassed10.79 s
test_v8311_performance_icons.py, test_v831_automotive_ref.py, test_v832_preview_easy_editor.pypassed6.62 s
test_v833_theme_integration.py, test_v8341_slide_specs.py, test_v834_visual_presets.pypassed4.56 s
test_v8_art_installer.py, test_v8_workflows.py, test_wordpress_baseline.pypassed3.79 s
test_worker_queue_v6.py, test_workspace_v810.py, test_workspace_v812.pypassed7.92 s
test_wp_complex_v820.py, test_wp_migration_v81.py, test_wp_migration_v82.pypassed6.69 s
test_wp_migration_v83.pypassed3.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('SiteForge V8.34 · 真正生成的主题布局 + +
SiteForge V8.34 · Banner 与产品卡片来自实际服务端区块渲染 · 仅演示布局,不连接业务后台
+''' +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 %} - -
- -
- BRAND PROFILE / PARTNERS -

{{ page.title }}

-

{{ tp_brand_lead }}

- {% if related %}View brand products {{ icon('arrow-right') }}{% endif %} -
-
-
-{% if related %} -
-
-
-
- PRODUCT LINE -

{{ page.title }} products

-
- All products {{ icon('arrow-right', cls='sf-tp-icon--sm') }} -
-
- {% for tp_item in related[:8] %} - - {% endfor %} -
-
-
-{% else %} -
-
-
-

No products listed under this brand yet

-

Products assigned to this brand appear here automatically once published.

- Browse all products {{ icon('arrow-right') }} -
-
-
-{% 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 %} - -
- -
- 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 %} -
-
- STILL NEED HELP? -

Can't find your answer?

-

Send the question directly — replies within one business day.

-
- Contact us {{ icon('arrow-right') }} -
-
-
+
SUPPORT / FAQ

{{page.title}}

{{page.summary}}

+
ANSWERS

Frequently asked questions.

Cooperation process, trade terms and common questions.

{{body_copy(content,page)}}{% if page.cover %}{{page.title}}{% 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:找不到页面的语义),无裸箭头字符。 #} -
-
-
- - ERROR / 404 -

404

-

The page you are looking for does not exist or has been moved.

- -
-
-
+
ERROR / 404

404

The page you are looking for does not exist or has been moved.

Back to home {{ icon('arrow-right') }}
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 带。页面为英文 - (构建器的中文页题「站内搜索」仅进 ,不进正文)。 #} -<section class="sf-tp-page-hero sf-tp-search-hero"> - <div class="sf-tp-page-hero-overlay" aria-hidden="true"></div> - <div class="wrap"> - <div class="sf-tp-page-hero-copy" data-reveal> - <span class="eyebrow">SEARCH / CONTENT</span> - <h1>Site Search</h1> - <p class="sf-tp-page-hero-lead">Search products, brands, news and cases across this site.</p> - </div> - </div> -</section> -<main id="main-content" class="sf-search-page sf-tp-search-panel"> - <label for="sf-search">Enter a keyword</label> - <input type="search" id="sf-search" placeholder="Search products, brands, news, cases..." autofocus> - <div id="sf-search-count" aria-live="polite"></div> - <div id="sf-search-results"></div> -</main> +<section class="pro-inner-hero"><div class="pro-wrap"><span class="pro-eyebrow">SEARCH / CONTENT</span><h1>Site Search</h1><p>Search products, brands, news and cases across this site.</p></div></section> +<main id="main-content" class="sf-search-page pro-wrap"><label for="sf-search">Enter a keyword</label><input type="search" id="sf-search" placeholder="Search products, brands, news, cases..." autofocus><div id="sf-search-count" aria-live="polite"></div><div id="sf-search-results"></div></main> 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 › 标题)。 #} -<section class="sf-tp-page-hero sf-tp-service-hero"> - {% if safe_image(page.cover) %}<img class="sf-tp-page-hero-bg" src="{{ safe_image(page.cover) }}" alt="" width="1600" height="900" fetchpriority="high" decoding="async">{% endif %} - <div class="sf-tp-page-hero-overlay" aria-hidden="true"></div> - <div class="wrap"> - <nav class="sf-tp-breadcrumb" aria-label="Breadcrumb" data-reveal> - <a href="/">Home</a>{% if route != '/services/' %}<a href="/services/">Services</a>{% endif %}<span class="sf-tp-breadcrumb-current">{{ page.title }}</span> - </nav> - <div class="sf-tp-page-hero-copy" data-reveal style="--reveal-i: 1"> - <span class="eyebrow">SERVICE / SUPPORT</span> - <h1>{{ page.title }}</h1> - {% if page.summary %}<p class="sf-tp-page-hero-lead">{{ page.summary }}</p>{% endif %} - </div> - </div> -</section> -<section class="section sf-tp-service"> - <div class="wrap sf-tp-service-grid{{ '' if content else ' sf-tp-service-grid--solo' }}"> - {% if content %} - <div class="sf-tp-service-copy" data-reveal> - <span class="eyebrow">SERVICE DETAIL</span> - <h2>{{ page.title }}</h2> - <div class="sf-tp-service-body sf-tp-article-body" data-reveal style="--reveal-i: 1">{{ body_copy(content, page) }}</div> - </div> - {% endif %} - <aside class="sf-tp-service-aside" data-reveal style="--reveal-i: 1"> - <div class="sf-tp-service-cta"> - <span class="eyebrow">NEXT STEP</span> - <h3>Request this service</h3> - <p>Send your requirements and the team replies within one business day.</p> - <a class="sf-tp-btn" href="/contact/">Contact us {{ icon('arrow-right') }}</a> - </div> - {% if route != '/services/' %}<a class="sf-tp-service-back" href="/services/">{{ icon('arrow-left', cls='sf-tp-icon--sm') }} All services</a>{% endif %} - </aside> - </div> -</section> +<section class="pro-inner-hero pro-info-hero"><div class="pro-wrap"><span class="pro-eyebrow">OUR SERVICES</span><h1>{{page.title}}</h1><p>{{page.summary}}</p></div></section> +<section class="pro-section pro-info-section"><div class="pro-wrap pro-info-grid"><div><span class="pro-eyebrow">SERVICE / SUPPORT</span><h2>{{page.title}}</h2><p class="pro-muted">Working process, service scope and support commitments.</p></div><div class="pro-info-copy">{{body_copy(content,page)}}{% if page.cover %}<img class="pro-info-photo" src="{{safe_image(page.cover)}}" alt="{{page.title}}">{% endif %}</div></div></section> 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} -}