upgrade: adopt V8.33.1 — final merge, our patches upstreamed natively

V8.33.0 absorbed our entire platform patch set as native features
(discovery/env_for macro override/blocks.css priority/type-mapping fix/
frontend entries/locale override) and synced trade-pro from our remote;
V8.33.1 adds HESU PIPE industrial-theme home fixes (1.3.1).

Zero patch re-application this round — all verified native. Theme
suites 35/35; full run 428 passed + same 10 known Windows env fails
(FAILED set unchanged). Live: container 8.33.1-dev.1 healthy, hesupipe
192 entries intact, trade-pro build 203 pages 0 errors.

Patch registry retired to archive — future upgrades are plain overlay.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-26 14:27:17 +08:00
parent b5996bf2b1
commit e21dbb6518
66 changed files with 2137 additions and 24 deletions
+44
View File
@@ -0,0 +1,44 @@
# SiteForge Enterprise V8.33 — 五套主题安全融合与主线稳定性
**基线:V8.32.0-dev.1;交付:V8.33.0-dev.1;新增主题:Trade Pro 1.0.0。**
此项目包含完整 FastAPI/Worker 网站工厂源码、5 套内置主题、编辑器、迁移、数据装配、审批构建、测试和验收报告。不是仅有 Trade Pro 的增量包。
## 本轮实装
- V8.32 主线之上,选择性引入 Trade Pro 主题 14 类布局与 13 类页面预设;保留旧主题各版本和已有编辑器/发布功能。
- 主题发现统一为 `themes/*/manifest.json`(`app/core.py`),核心、专业主题、主题商店共用一个主题清单。
- 主题本地组件宏覆盖共享宏;上传的自定义主题仍限制为声明式 CSS/预设,不能上传任意 Jinja 或脚本。
- 后台工作台、主题图库、编辑器、建站向导补齐第五主题选项、说明和色板。
- 为 Trade Pro 增加受控的 `blocks.css`,构建和主题预览共享同一来源;历史汽车主题版本与优化资源包不被改变。
- 支持独立英文站点的语言设置,Trade Pro 默认 `en`(语言组配置优先),保留路径与 SEO 字段;修复只选取 settings 字段导致构建报错。
- 保留 V8.32 主题预览 iframe 的同源安全限制与编辑器 `sandbox=allow-scripts`(**不授予同源脚本权限**)。
## 主要文件
- 新主题 `themes/trade-pro/`(manifest、预设、14 个页面、主题 CSS、blocks.css、组件宏)
- 主题服务 `app/core.py`、`app/theme_pro.py`、`app/theme_store.py`、`app/factory_api.py`、`app/builder.py`
- UI `app/templates/theme-gallery.html`、`app/templates/editor.html`、`app/static/studio-experience-v829.js`
- 测试 `tests/test_theme_registry.py`、`tests/test_v833_theme_integration.py`
- 隔离演示 `examples/V8_33_五套主题真实页面_离线交互预览.html`
- 验收 `reports/v833/V8_33_五套主题融合验收.html`、`reports/v833-regression/`
## 验收(本机开发/隔离环境)
- Python:**435 passed,19/19 独立批次通过**(`scripts/regression_v833.py`)。
- JavaScript:**39 项通过**,嵌套组件额外专项通过。
- HTTP + Worker:真实启动、登录、CSRF、建站、异步入队、Worker 生成 Trade Pro 静态 HTML、ZIP、预览通过。
- Chromium:实际渲染的 Trade Pro 产品详情在电脑 1365px 与手机 390px 测试通过,外层无横向溢出、脚本错误 0;此浏览器测试是**离线隔离**环境,不是正式登录后台 E2E。
- 主题:5 × 14 页面类型可通过后台 API 生成;离线演示可切换 5 × 14 × 3 设备(设备宽度为模拟视口)。
## 运行
安装依赖:`pip install -r requirements-dev.lock`(按 Python 项目自带说明设置数据目录、环境变量)。
测试:`python -m pytest -q tests/test_v833_theme_integration.py tests/test_theme_registry.py`。
回归:`python scripts/regression_v833.py --timeout 90 --group-size 3`。
真实 HTTP/Worker 冒烟:`python scripts/http_v833_tradepro.py`。
可直接打开 `examples/V8_33_五套主题真实页面_离线交互预览.html`。
## 尚未完成 / 下阶段
- 上传计划所述 `D:\project\cms` 原始 Astro 工程不在交付包,像素级还原无法完成正式对照。
- 企业真实完整内容(例如 193 条样本)及原站链接、媒体、SEO 差异尚未做完整业务验收;未从上传包直接移植数据库或企业数据。
- Art Design Pro 官方工程完整编译、真实 PostgreSQL/Redis 多 Worker、正式服务器部署/回滚/权限 E2E、Web Vitals 真实网络性能仍未验收。
- Trade Pro 专用 blocks.css 现沿用平台稳定规则加少量语义化扩展,设计精细度需按 V8.34 再完善;离线演示使用虚构数据且禁用 iframe 内脚本。
**严格遵守:不自动替换已有网站主题,不覆盖固定历史版本,不改写导入 URL/SEO,不把技术功能隐藏为无法理解的操作。**
+33
View File
@@ -0,0 +1,33 @@
# SiteForge Enterprise V8.33.1 — HESU PIPE 首页布局专项修复
**基线**:V8.33.0-dev.1。**本补丁版本**:V8.33.1-dev.1。**工业主题**:1.3.0 → 1.3.1。
## 修复原因(已通过实际构建确认)
1. 原渲染结果同时具有 `<section class="sf-section sf-collection">` 和内部 `<div class="sf-collection">`。共有 CSS 将二者都设为网格,导致整个 section 自身出现多列,内容宽度被压缩。同样的问题影响 `.sf-features`、`.sf-stats` 和部分其他区块。
2. 浏览器运行时代码会对所有 `.sf-collection` 插入筛选工具栏,导致首页展示区也出现产品筛选,并可能重复作用于外层 section。
3. 工业建材主题缺少与其页面视觉一致的核心优势和产品展示专用样式,卡片列数、图片比例与文字高度在不同设备下不能合理协调。
## 实际代码调整
- `app/static/blocks.css`:禁止区块外层误套内部 Grid 布局;维持内部网格。
- `app/blueprints.py`:核心优势根据 1~4 条目选择相应桌面列数;动态列表根据页面用途决定是否显示筛选,首页只呈现产品卡片。
- `app/static/site-runtime.js`:仅对 `data-sf-filterable="true"` 的内部网格启用筛选。
- `themes/industrial/blocks.css`:独立工业主题卡片样式,适配 1440/820/390/375px;产品图片采用 `object-fit:contain` 不裁切管材。
- `themes/industrial/manifest.json`:升级至 `1.3.1`;`themes/industrial/versions/1.3.0/` 保存原 1.3.0 完整主题文件。
- `tests/test_hesupipe_layout_v8331.py`:增加 4 项专项测试;升级已硬编码主题版本号的 3 个历史测试模块。
## 用户端如何查看
在 SiteForge 后台找到 HESU PIPE 对应站点,检查其主题是否为 **工业制造 / Industrial**,然后在主题管理中选择新版本 **1.3.1**(这一步不自动修改网站内容)。进入首页可视化编辑器,确认“核心优势”和“动态内容列表”区块已经填好内容。保存草稿,再进行主题预览与构建。经预览检查、审批和部署完成后,线上才会更新。
> 旧站如固定 1.3.0,将继续使用原始布局。主题版本升级须由使用者明确选择,不直接修改现有站点或在线数据。
## 离线预览、验证范围
- `examples/V8_33_1_HesuPipe_首页布局真实构建预览.html`:使用真正的静态构建 HTML/CSS/JS,合成的示范站点(非线上实际数据),支持电脑、平板、手机宽度切换。
- `reports/v8331/hesupipe-home-blocks-1440.png` 和 `...-390.png`:实际 Chromium 浏览器截图。
- `reports/v8331/browser-results.json`:四种屏宽的 computed CSS 验证。
- `scripts/hesupipe_v8331_preview.py`、`scripts/hesupipe_v8331_browser.py`:可复现的构建和浏览器核验脚本。
**限制**:本次未取得 hesupipe.com 线上部署权限,也没有用户后台中的真实首页草稿和照片。公开的 hesupipe.com 当前为不同的已发布网站;本补丁针对 SiteForge 项目版本的组件渲染,不代表线上网站已更新。请在测试环境先检查真实图片、数据与移动端,再走现有审批发布流程。
+7 -2
View File
@@ -254,7 +254,9 @@ def render_blocks(blocks, site, page, entries):
elif typ=='features': elif typ=='features':
items=block.get('items') if isinstance(block.get('items'),list) else [] items=block.get('items') if isinstance(block.get('items'),list) else []
cards=''.join(f'<article class="sf-feature"><span>{i+1:02}</span><h3>{escape(str(item.get("title","")))}</h3><p>{escape(str(item.get("text","")))}</p></article>' for i,item in enumerate(items[:12]) if isinstance(item,dict)) cards=''.join(f'<article class="sf-feature"><span>{i+1:02}</span><h3>{escape(str(item.get("title","")))}</h3><p>{escape(str(item.get("text","")))}</p></article>' for i,item in enumerate(items[:12]) if isinstance(item,dict))
parts.append(start+f'<div class="sf-section-head"><h2>{title}</h2></div><div class="sf-features">{cards}</div>'+end) visible_items=sum(1 for item in items[:12] if isinstance(item,dict))
feature_cols=visible_items if 1<=visible_items<=4 else 3
parts.append(start+f'<div class="sf-section-head"><h2>{title}</h2></div><div class="sf-features" style="--sf-feature-cols:{feature_cols}">{cards}</div>'+end)
elif typ=='stats': elif typ=='stats':
items=block.get('items') if isinstance(block.get('items'),list) else [] items=block.get('items') if isinstance(block.get('items'),list) else []
cards=''.join(f'<div><strong>{escape(str(item.get("value","")))}</strong><span>{escape(str(item.get("label","")))}</span></div>' for item in items[:12] if isinstance(item,dict)) cards=''.join(f'<div><strong>{escape(str(item.get("value","")))}</strong><span>{escape(str(item.get("label","")))}</span></div>' for item in items[:12] if isinstance(item,dict))
@@ -269,7 +271,10 @@ def render_blocks(blocks, site, page, entries):
bound=[r for r in bound if brand_of(r)==str(page.get('title','')).casefold().strip()] bound=[r for r in bound if brand_of(r)==str(page.get('title','')).casefold().strip()]
count=max(1,min(48,int(block.get('limit') or 6)));cols=max(2,min(4,int(block.get('columns') or 3))) count=max(1,min(48,int(block.get('limit') or 6)));cols=max(2,min(4,int(block.get('columns') or 3)))
cards=''.join(f'<a class="sf-item" data-category="{escape(str((json.loads(r.get("meta_json") or "{}").get("fields") or {}).get("category", "")),quote=True)}" data-brand="{escape(str((json.loads(r.get("meta_json") or "{}").get("fields") or {}).get("brand", "")),quote=True)}" href="{safe_href(r.get("route") or "/")}"><div class="sf-item-img">'+(f'<img src="{safe_media(r.get("cover"))}" alt="{escape(r.get("title",""),quote=True)}" loading="lazy">' if safe_media(r.get('cover')) else '<div class="sf-placeholder">'+escape(r.get('title','')[:2])+'</div>')+f'</div><div class="sf-item-copy"><small>{escape(r.get("type",""))}</small><h3>{escape(r.get("title",""))}</h3><p>{escape((r.get("summary") or "")[:140])}</p><span>查看详情 →</span></div></a>' for r in bound[:count]) cards=''.join(f'<a class="sf-item" data-category="{escape(str((json.loads(r.get("meta_json") or "{}").get("fields") or {}).get("category", "")),quote=True)}" data-brand="{escape(str((json.loads(r.get("meta_json") or "{}").get("fields") or {}).get("brand", "")),quote=True)}" href="{safe_href(r.get("route") or "/")}"><div class="sf-item-img">'+(f'<img src="{safe_media(r.get("cover"))}" alt="{escape(r.get("title",""),quote=True)}" loading="lazy">' if safe_media(r.get('cover')) else '<div class="sf-placeholder">'+escape(r.get('title','')[:2])+'</div>')+f'</div><div class="sf-item-copy"><small>{escape(r.get("type",""))}</small><h3>{escape(r.get("title",""))}</h3><p>{escape((r.get("summary") or "")[:140])}</p><span>查看详情 →</span></div></a>' for r in bound[:count])
parts.append(start+f'<div class="sf-section-head"><div><span class="sf-kicker">{subtitle}</span><h2>{title}</h2></div><a href="{safe_href(resolve("href","/product/"))}">{escape(resolve("button","查看全部"))} →</a></div><div class="sf-collection" style="--sf-cols:{cols}">{cards if cards else "<p class=\"sf-muted\">当前暂无相关内容,请导入或添加数据。</p>"}</div>'+end) # 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'<div class="sf-section-head"><div><span class="sf-kicker">{subtitle}</span><h2>{title}</h2></div><a href="{safe_href(resolve("href","/product/"))}">{escape(resolve("button","查看全部"))}</a></div><div class="sf-collection" data-sf-filterable="{str(filterable).lower()}" style="--sf-cols:{cols}">{cards if cards else "<p class=\"sf-muted\">当前暂无相关内容,请导入或添加数据。</p>"}</div>'+end)
elif typ=='image_text': elif typ=='image_text':
im=safe_media(resolve('image')); img=f'<img src="{im}" alt="{title}" loading="lazy">' if im else '<div class="sf-placeholder sf-large">✧</div>' im=safe_media(resolve('image')); img=f'<img src="{im}" alt="{title}" loading="lazy">' if im else '<div class="sf-placeholder sf-large">✧</div>'
side=' sf-reverse' if block.get('image_side')=='left' else '' side=' sf-reverse' if block.get('image_side')=='left' else ''
+11 -1
View File
@@ -269,6 +269,16 @@ def theme_preview_css(theme_id:str,version:str=''):
except ValueError as exc:raise HTTPException(404,str(exc)) except ValueError as exc:raise HTTPException(404,str(exc))
return Response((dir/'theme.css').read_text(encoding='utf-8'),media_type='text/css') return Response((dir/'theme.css').read_text(encoding='utf-8'),media_type='text/css')
@router.get('/themes/{theme_id}/preview.blocks.css')
def theme_preview_blocks_css(theme_id:str,version:str=''):
"""Serve the identical block stylesheet used by a static theme build."""
from fastapi.responses import Response
try:folder=theme_store.resolve(theme_id,version or None)
except ValueError as exc:raise HTTPException(404,str(exc))
chosen=folder/'blocks.css'
if not chosen.is_file():chosen=theme_store.BASE.parent/'app'/'static'/'blocks.css'
return Response(chosen.read_text(encoding='utf-8'),media_type='text/css',headers={'Cache-Control':'no-store'})
@router.get('/themes/{theme_id}/preview.pro.css') @router.get('/themes/{theme_id}/preview.pro.css')
def theme_preview_pro_css(theme_id:str,version:str=''): def theme_preview_pro_css(theme_id:str,version:str=''):
from fastapi.responses import Response from fastapi.responses import Response
@@ -367,6 +377,6 @@ def theme_preview(theme_id:str,version:str='',page_type:str='home'):
seo_description='SiteForge 主题模板预览') seo_description='SiteForge 主题模板预览')
html=static_site.shell(html,site,page,records,[]) html=static_site.shell(html,site,page,records,[])
html=html.replace('assets/theme.css',f'/api/factory/themes/{theme_id}/preview.css?version={version or theme_store.latest(theme_id)}') html=html.replace('assets/theme.css',f'/api/factory/themes/{theme_id}/preview.css?version={version or theme_store.latest(theme_id)}')
html=html.replace('assets/blocks.css','/ui/blocks.css').replace('assets/factory.css','/ui/factory.css') html=html.replace('assets/blocks.css',f'/api/factory/themes/{theme_id}/preview.blocks.css?version={version or theme_store.latest(theme_id)}').replace('assets/factory.css','/ui/factory.css')
html=html.replace('assets/custom.css','/ui/factory.css').replace('assets/site-runtime.js','/ui/site-runtime.js') html=html.replace('assets/custom.css','/ui/factory.css').replace('assets/site-runtime.js','/ui/site-runtime.js')
return HTMLResponse(html) return HTMLResponse(html)
+1 -1
View File
@@ -540,7 +540,7 @@ def editor_preview(site_id:str,entry_id:str,payload:BlueprintPayload):
styles='\n'.join([ styles='\n'.join([
(ROOT/'app'/'static'/'pro-base.css').read_text(encoding='utf-8'), (ROOT/'app'/'static'/'pro-base.css').read_text(encoding='utf-8'),
(ROOT/'app'/'static'/'commercial.css').read_text(encoding='utf-8'), (ROOT/'app'/'static'/'commercial.css').read_text(encoding='utf-8'),
(ROOT/'app'/'static'/'blocks.css').read_text(encoding='utf-8'), ((theme_dir/'blocks.css') if (theme_dir/'blocks.css').is_file() else (ROOT/'app'/'static'/'blocks.css')).read_text(encoding='utf-8'),
(ROOT/'app'/'static'/'factory.css').read_text(encoding='utf-8'), (ROOT/'app'/'static'/'factory.css').read_text(encoding='utf-8'),
(theme_dir/'pro.css').read_text(encoding='utf-8') if (theme_dir/'pro.css').exists() else '', (theme_dir/'pro.css').read_text(encoding='utf-8') if (theme_dir/'pro.css').exists() else '',
(theme_dir/'theme.css').read_text(encoding='utf-8'), (theme_dir/'theme.css').read_text(encoding='utf-8'),
+3 -2
View File
@@ -97,7 +97,7 @@ def set_site_locale(site_id:str,data:LocaleIn,request:Request):
enterprise.require(request,site_id,'owner') enterprise.require(request,site_id,'owner')
try:locale=locale_value(data.locale) try:locale=locale_value(data.locale)
except ValueError as exc:raise HTTPException(400,str(exc)) except ValueError as exc:raise HTTPException(400,str(exc))
site=db.one('SELECT settings_json FROM sites WHERE id=?',(site_id,)) site=db.one('SELECT settings_json,theme FROM sites WHERE id=?',(site_id,))
if not site:raise HTTPException(404,'网站不存在') if not site:raise HTTPException(404,'网站不存在')
settings=json.loads(site['settings_json'] or '{}') settings=json.loads(site['settings_json'] or '{}')
settings['locale']=locale settings['locale']=locale
@@ -195,12 +195,13 @@ def site_locale(site_id):
row=db.one('SELECT locale FROM language_sites WHERE site_id=?',(site_id,)) row=db.one('SELECT locale FROM language_sites WHERE site_id=?',(site_id,))
if row:return row['locale'] if row:return row['locale']
# Per-site locale override (e.g. standalone English sites outside any language group) # Per-site locale override (e.g. standalone English sites outside any language group)
site=db.one('SELECT settings_json FROM sites WHERE id=?',(site_id,)) site=db.one('SELECT settings_json,theme FROM sites WHERE id=?',(site_id,))
try:override=json.loads(site['settings_json'] or '{}').get('locale') if site else None try:override=json.loads(site['settings_json'] or '{}').get('locale') if site else None
except ValueError:override=None except ValueError:override=None
if override: if override:
try:return locale_value(override) try:return locale_value(override)
except ValueError:pass except ValueError:pass
if site and site['theme']=='trade-pro':return 'en'
return 'zh-CN' return 'zh-CN'
def assert_build_safe(site_id): def assert_build_safe(site_id):
+6
View File
@@ -27,3 +27,9 @@
.sf-map-address{font-style:normal;font-size:18px;white-space:pre-line;line-height:1.8} .sf-map-address{font-style:normal;font-size:18px;white-space:pre-line;line-height:1.8}
@media (prefers-reduced-motion:reduce){.sf-slides{scroll-behavior:auto}.sf-item{transition:none}} @media (prefers-reduced-motion:reduce){.sf-slides{scroll-behavior:auto}.sf-item{transition:none}}
@media(max-width:540px){.sf-slide{flex-basis:92%}} @media(max-width:540px){.sf-slide{flex-basis:92%}}
/* V8.33.1 grid-class collision fix: block section and inner collection/features
share a class. The section must NOT become a grid container itself.
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}
+1 -1
View File
@@ -284,7 +284,7 @@ const Studio={
}, },
filterPalette(value){paletteSearch=String(value||'').slice(0,70);Studio.renderPalette()}, filterPalette(value){paletteSearch=String(value||'').slice(0,70);Studio.renderPalette()},
clearPaletteSearch(){paletteSearch='';const inp=$('#paletteSearch');if(inp){inp.value='';inp.focus()}Studio.renderPalette()}, clearPaletteSearch(){paletteSearch='';const inp=$('#paletteSearch');if(inp){inp.value='';inp.focus()}Studio.renderPalette()},
showThemePreview(){const dialog=$('#themePreviewModal');if(!dialog)return;const current=['automotive','industrial','corporate','minimal'].includes(site?.theme)?site.theme:'corporate';$('#themePreviewTheme').value=current;if(!dialog.open)dialog.showModal();Studio.themeDevice('desktop');Studio.updateThemePreview()}, showThemePreview(){const dialog=$('#themePreviewModal');if(!dialog)return;const current=['automotive','industrial','corporate','minimal','trade-pro'].includes(site?.theme)?site.theme:'corporate';$('#themePreviewTheme').value=current;if(!dialog.open)dialog.showModal();Studio.themeDevice('desktop');Studio.updateThemePreview()},
closeThemePreview(){themePreviewSeq++;themePreviewAbort?.abort();const dialog=$('#themePreviewModal');if(dialog?.open)dialog.close();const frame=$('#themePreviewFrame');if(frame){frame.removeAttribute('src');frame.removeAttribute('srcdoc')}}, closeThemePreview(){themePreviewSeq++;themePreviewAbort?.abort();const dialog=$('#themePreviewModal');if(dialog?.open)dialog.close();const frame=$('#themePreviewFrame');if(frame){frame.removeAttribute('src');frame.removeAttribute('srcdoc')}},
themeDevice(device){ themeDevice(device){
const width={desktop:'100%',tablet:'780px',phone:'390px'}[device];if(!width)return; const width={desktop:'100%',tablet:'780px',phone:'390px'}[device];if(!width)return;
+1 -1
View File
@@ -3,7 +3,7 @@
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)]; 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);});} 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);});}
// Filter actual static content cards, no API is needed at runtime. // Filter actual static content cards, no API is needed at runtime.
$$('.sf-collection').forEach(grid=>{ $$('.sf-collection[data-sf-filterable="true"]').forEach(grid=>{
const cards=$$('.sf-item',grid); const cards=$$('.sf-item',grid);
if(cards.length<2)return; if(cards.length<2)return;
const toolbar=document.createElement('div');toolbar.className='sf-filters'; const toolbar=document.createElement('div');toolbar.className='sf-filters';
+3 -2
View File
@@ -1,7 +1,7 @@
/* SiteForge V8.29: presentation-only studio helpers. No persisted-schema changes. */ /* SiteForge V8.29: presentation-only studio helpers. No persisted-schema changes. */
'use strict'; 'use strict';
(function(root){ (function(root){
const themes=new Set(['automotive','industrial','corporate','minimal']); const themes=new Set(['automotive','industrial','corporate','minimal','trade-pro']);
const pages=new Set(['home','listing','product','article','about','contact']); const pages=new Set(['home','listing','product','article','about','contact']);
const categories={ const categories={
visual:new Set(['hero','banner','carousel','gallery','image','image_text','image-grid','video','slider']), visual:new Set(['hero','banner','carousel','gallery','image','image_text','image-grid','video','slider']),
@@ -29,7 +29,8 @@
automotive:['#ffffff','#14283d','#f3f6f9','#102e47'], automotive:['#ffffff','#14283d','#f3f6f9','#102e47'],
industrial:['#ffffff','#193f59','#eef5f8','#104a6d'], industrial:['#ffffff','#193f59','#eef5f8','#104a6d'],
corporate:['#ffffff','#262d58','#f4f3fb','#34397b'], corporate:['#ffffff','#262d58','#f4f3fb','#34397b'],
minimal:['#faf9f5','#292924','#f0efe9','#30302c'] minimal:['#faf9f5','#292924','#f0efe9','#30302c'],
'trade-pro':['#ffffff','#17365c','#eef4fc','#0b3d91']
}; };
function stylePreset(theme,preset){ function stylePreset(theme,preset){
const [base,ink,soft,dark]=designPalettes[themes.has(theme)?theme:'corporate']; const [base,ink,soft,dark]=designPalettes[themes.has(theme)?theme:'corporate'];
+1 -1
View File
@@ -55,7 +55,7 @@
</div> </div>
<dialog id="themePreviewModal" class="sf-theme-showcase" aria-labelledby="themePreviewTitle"> <dialog id="themePreviewModal" class="sf-theme-showcase" aria-labelledby="themePreviewTitle">
<div class="sf-showcase-head"><div><small>仅预览 · 不修改网站内容</small><h2 id="themePreviewTitle">选择适合网站的主题效果</h2><p>先比较布局和手机显示效果,再决定是否到「网站设计」中应用主题。</p></div><button type="button" class="ui-btn ghost" onclick="Studio.closeThemePreview()" aria-label="关闭主题预览">关闭 ×</button></div> <div class="sf-showcase-head"><div><small>仅预览 · 不修改网站内容</small><h2 id="themePreviewTitle">选择适合网站的主题效果</h2><p>先比较布局和手机显示效果,再决定是否到「网站设计」中应用主题。</p></div><button type="button" class="ui-btn ghost" onclick="Studio.closeThemePreview()" aria-label="关闭主题预览">关闭 ×</button></div>
<div class="sf-showcase-tools"><label>主题风格<select id="themePreviewTheme" onchange="Studio.updateThemePreview()"><option value="automotive">汽车与出行</option><option value="industrial">工业与制造</option><option value="corporate">企业品牌</option><option value="minimal">简约展示</option></select></label><label>参考页面<select id="themePreviewPage" onchange="Studio.updateThemePreview()"><option value="home">首页</option><option value="listing">产品列表</option><option value="product">产品详情</option><option value="article">文章详情</option><option value="about">关于我们</option><option value="contact">联系我们</option></select></label><div class="sf-showcase-device" role="group" aria-label="主题参考设备"><button type="button" data-theme-device="desktop" aria-pressed="true" onclick="Studio.themeDevice('desktop')">电脑</button><button type="button" data-theme-device="tablet" aria-pressed="false" onclick="Studio.themeDevice('tablet')">平板</button><button type="button" data-theme-device="phone" aria-pressed="false" onclick="Studio.themeDevice('phone')">手机</button></div></div> <div class="sf-showcase-tools"><label>主题风格<select id="themePreviewTheme" onchange="Studio.updateThemePreview()"><option value="automotive">汽车与出行</option><option value="industrial">工业与制造</option><option value="corporate">企业品牌</option><option value="minimal">简约展示</option><option value="trade-pro">Trade Pro · 外贸 B2B</option></select></label><label>参考页面<select id="themePreviewPage" onchange="Studio.updateThemePreview()"><option value="home">首页</option><option value="listing">产品列表</option><option value="product">产品详情</option><option value="article">文章详情</option><option value="about">关于我们</option><option value="contact">联系我们</option></select></label><div class="sf-showcase-device" role="group" aria-label="主题参考设备"><button type="button" data-theme-device="desktop" aria-pressed="true" onclick="Studio.themeDevice('desktop')">电脑</button><button type="button" data-theme-device="tablet" aria-pressed="false" onclick="Studio.themeDevice('tablet')">平板</button><button type="button" data-theme-device="phone" aria-pressed="false" onclick="Studio.themeDevice('phone')">手机</button></div></div>
<p class="sf-showcase-status" id="themePreviewStatus" role="status" aria-live="polite">示例使用主题最新版本,可能与你网站锁定的主题版本不同。不会修改当前页面。</p><div class="sf-showcase-stage"><div id="themePreviewFrameShell" class="sf-showcase-frame"><iframe id="themePreviewFrame" title="主题示例页面" sandbox="allow-scripts" referrerpolicy="no-referrer" loading="lazy"></iframe></div></div> <p class="sf-showcase-status" id="themePreviewStatus" role="status" aria-live="polite">示例使用主题最新版本,可能与你网站锁定的主题版本不同。不会修改当前页面。</p><div class="sf-showcase-stage"><div id="themePreviewFrameShell" class="sf-showcase-frame"><iframe id="themePreviewFrame" title="主题示例页面" sandbox="allow-scripts" referrerpolicy="no-referrer" loading="lazy"></iframe></div></div>
<div class="sf-showcase-footer"><span>编辑器仅查看外观;完整图库支持交互。预览不会保存或发布主题。<button type="button" id="themePreviewRetry" class="ui-btn ghost" onclick="Studio.updateThemePreview()" hidden>重新加载</button></span><a class="ui-btn ghost" href="/theme-gallery" target="_blank" rel="noopener">打开完整主题图库 ↗</a></div> <div class="sf-showcase-footer"><span>编辑器仅查看外观;完整图库支持交互。预览不会保存或发布主题。<button type="button" id="themePreviewRetry" class="ui-btn ghost" onclick="Studio.updateThemePreview()" hidden>重新加载</button></span><a class="ui-btn ghost" href="/theme-gallery" target="_blank" rel="noopener">打开完整主题图库 ↗</a></div>
</dialog> </dialog>
+1 -1
View File
@@ -45,6 +45,6 @@ theme.addEventListener('change',loadVersions);[page,version].forEach(control=>co
retry.addEventListener('click',show); retry.addEventListener('click',show);
document.querySelectorAll('.devices button').forEach(button=>button.addEventListener('click',()=>{document.querySelectorAll('.devices button').forEach(other=>other.classList.remove('active'));button.classList.add('active');frame.style.width=button.dataset.size;})); document.querySelectorAll('.devices button').forEach(button=>button.addEventListener('click',()=>{document.querySelectorAll('.devices button').forEach(other=>other.classList.remove('active'));button.classList.add('active');frame.style.width=button.dataset.size;}));
const requestedTheme=new URLSearchParams(location.search).get('theme'); const requestedTheme=new URLSearchParams(location.search).get('theme');
if(['automotive','industrial','corporate','minimal'].includes(requestedTheme))theme.value=requestedTheme; if(['automotive','industrial','corporate','minimal','trade-pro'].includes(requestedTheme))theme.value=requestedTheme;
loadVersions(); loadVersions();
</script></body></html> </script></body></html>
+1 -1
View File
@@ -1,5 +1,5 @@
"""Single source of truth for the SiteForge platform release identity.""" """Single source of truth for the SiteForge platform release identity."""
import os import os
VERSION = "8.32.0-dev.1" VERSION = "8.33.1-dev.1"
PLATFORM_NAME = "SiteForge Factory" PLATFORM_NAME = "SiteForge Factory"
BUILD_SHA = os.environ.get("SITEFORGE_GIT_SHA", "unknown").strip()[:40] or "unknown" BUILD_SHA = os.environ.get("SITEFORGE_GIT_SHA", "unknown").strip()[:40] or "unknown"
+23
View File
@@ -0,0 +1,23 @@
# V8.33–V8.38 融合执行计划(以用户上传计划为依据)
## 优先级与状态
| 阶段 | 项目 | 本阶段状态 | 下一步交付要求 |
|---|---|---|---|
| P0 | 安全回归、历史主题、数据隔离 | 本轮隔离测试通过 | 真实授权用户权限回归/发布演练 |
| P1 | 主题管理与平台底座 | 五主题主线注册/预览/构建已集成 | 主题自动化升级、资源缓存基准 |
| P2 | WordPress 完整迁移 | 既有能力保留但未在真实站验收 | 真实 XML/媒体/SEO/旧路径/重定向差异表 |
| P3 | 数据装配 | 既有字段映射保留 | 真实 Excel 车型/品牌/产品映射误差验收 |
| P4 | 主题与高级可视化编辑 | V8.32 简洁编辑保留、第五主题接入 | V8.34 图片/Banner/菜单/规格直接编辑,预览→构建对照 |
| P5 | 审批、构建、部署、回滚 | HTTP+Worker 冒烟通过 | V8.37 真实 SFTP/S3/Agent 部署与回滚 |
| P6 | 多语言与 SEO | 语言组+站点默认语言保留 | V8.36 阿语/俄语/英文页面及 hreflang 校验 |
| P7 | 企业级生产化 | 尚未验收 | V8.38+ PG/Redis/多 Worker、Art Design Pro 官方工程整合、安全压测 |
## V8.34 下一轮重点
1. 五套主题的页面完整性与行业差异,重点将 Trade Pro 14 类页面用于真实内容而非样例。
2. 可视化编辑中的图片、Banner、导航、产品参数直接修改;默认简洁、按需高级设置。
3. 同一网站同一版本的主题预览、编辑器和静态构建画面对照测试;旧站已固定主题不变。
4. 指标:CSS/JS 体积、图片宽高与懒加载、LCP/CLS/INP 实网采集;语义化图标不得到处使用箭头。
5. 真实网站迁移需保存 URL、Title、Description、Canonical、结构化数据、媒体路径,输出差异表。
本文件是更新后的主线计划。`docs/COMPLETION_PLAN.md` 是用户上传的历史原始规划,包含过时版本和本地路径,必须以最新源码、测试和实际环境验收为准;不能把旧计划中勾选项目直接算作本轮已完成。
@@ -399,7 +399,9 @@ def test_theme_color_map_covers_trade_pro():
4. noindex 空栏目:填真实内容或从导航移除,不留空页 4. noindex 空栏目:填真实内容或从导航移除,不留空页
5. 生产部署包只含构建产物与运行所需文件(不带 tests/examples/reports/demo) 5. 生产部署包只含构建产物与运行所需文件(不带 tests/examples/reports/demo)
## 补丁登记(2026-10-23 V8_21_3 升级后现行清单,下次升级重放) ## 补丁登记(已退役存档,2026-10-26 终局合并)
V8.33.0 起上游原生吸收了我们的全部平台补丁(主题发现/env_for 宏覆盖/blocks.css 优先/类型修复/前端入口/locale 覆盖,trade-pro 主题同步自我们的 Git 仓库)——**补丁重放时代结束**,此后升级=纯覆盖+验证原生实现+compose 钉名(siteforge-v8-10 项目名 + Web 侧 BUILD_TIMEOUT 透传)+双基线回归。历史清单见 git 历史(601451f/8884e10/95982fa)。
1. `app/core.py` discover_themes() 单一源;2. `app/theme_pro.py` env_for + theme_color + THEMES import;3. `app/theme_store.py` BUILTIN import;4. `app/builder.py` copy_blocks_css;5. `app/v8_workflows.py` 类型/分类;6. `app/multilingual.py` 站点级 locale 覆盖 + `PUT /api/i18n/sites/{id}/locale`(V8.15 locale 改写管线下英文站的正解);7. 前端 6 入口 + workspace.css 预览色;8. `tests/test_theme_pro_v3.py` 三处;9. compose 项目名钉 siteforge-v8-10 + .gitignore 追加;10. 保留目录:themes/trade-pro、tests/test_theme_registry.py、docs/superpowers。 1. `app/core.py` discover_themes() 单一源;2. `app/theme_pro.py` env_for + theme_color + THEMES import;3. `app/theme_store.py` BUILTIN import;4. `app/builder.py` copy_blocks_css;5. `app/v8_workflows.py` 类型/分类;6. `app/multilingual.py` 站点级 locale 覆盖 + `PUT /api/i18n/sites/{id}/locale`(V8.15 locale 改写管线下英文站的正解);7. 前端 6 入口 + workspace.css 预览色;8. `tests/test_theme_pro_v3.py` 三处;9. compose 项目名钉 siteforge-v8-10 + .gitignore 追加;10. 保留目录:themes/trade-pro、tests/test_theme_registry.py、docs/superpowers。
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+215
View File
@@ -0,0 +1,215 @@
{
"version": "8.33.1-dev.1",
"generated_utc": "2026-10-26T05:43:38.989499+00:00",
"groups": [
{
"batch": 1,
"files": [
"test_commercial_v5.py",
"test_connectors_v84.py",
"test_connectors_v85.py"
],
"status": "passed",
"seconds": 6.15,
"summary": "................................... [100%]\n35 passed in 3.85s\n\n"
},
{
"batch": 2,
"files": [
"test_console_v819.py",
"test_core_v4.py",
"test_deep_nested_v824.py"
],
"status": "passed",
"seconds": 6.24,
"summary": "........................... [100%]\n27 passed in 3.81s\n\n"
},
{
"batch": 3,
"files": [
"test_editor_easy_v8212.py",
"test_editor_ux_v8211.py",
"test_enterprise.py"
],
"status": "passed",
"seconds": 4.35,
"summary": "........... [100%]\n11 passed in 2.05s\n\n"
},
{
"batch": 4,
"files": [
"test_enterprise_v7.py",
"test_factory.py",
"test_factory_v2.py"
],
"status": "passed",
"seconds": 8.37,
"summary": "................................... [100%]\n35 passed in 5.92s\n\n"
},
{
"batch": 5,
"files": [
"test_hesupipe_layout_v8331.py",
"test_mobile_tabs_v8213.py",
"test_multilingual_v815.py"
],
"status": "passed",
"seconds": 6.84,
"summary": "........................ [100%]\n24 passed in 4.35s\n\n"
},
{
"batch": 6,
"files": [
"test_nested_v823.py",
"test_p0_71.py",
"test_p0_backup.py"
],
"status": "passed",
"seconds": 3.49,
"summary": "......... [100%]\n9 passed in 1.06s\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.66,
"summary": "................................ [100%]\n32 passed in 5.37s\n\n"
},
{
"batch": 8,
"files": [
"test_production_v816.py",
"test_professional_ui_v811.py",
"test_regression_runner_v817.py"
],
"status": "passed",
"seconds": 4.9,
"summary": "................... [100%]\n19 passed in 2.46s\n\n"
},
{
"batch": 9,
"files": [
"test_release_native_v814.py",
"test_release_reviews_v7.py",
"test_release_v813.py"
],
"status": "passed",
"seconds": 4.0,
"summary": "..................... [100%]\n21 passed in 1.57s\n\n"
},
{
"batch": 10,
"files": [
"test_site_design_v87.py",
"test_siteforge_v817_audit.py",
"test_studio_v86.py"
],
"status": "passed",
"seconds": 6.38,
"summary": "............................................. [100%]\n45 passed in 4.09s\n\n"
},
{
"batch": 11,
"files": [
"test_theme_pro_v3.py",
"test_theme_registry.py",
"test_v818_pg_recovery.py"
],
"status": "passed",
"seconds": 12.3,
"summary": ".......................................... [100%]\n42 passed in 9.88s\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.74,
"summary": "............ [100%]\n12 passed in 1.37s\n\n"
},
{
"batch": 13,
"files": [
"test_v825_shared_components.py",
"test_v826_global_design.py",
"test_v827_guided_workspace.py"
],
"status": "passed",
"seconds": 4.77,
"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.52,
"summary": "........... [100%]\n11 passed in 7.42s\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.72,
"summary": ".......... [100%]\n10 passed in 3.59s\n\n"
},
{
"batch": 16,
"files": [
"test_v833_theme_integration.py",
"test_v8_art_installer.py",
"test_v8_workflows.py"
],
"status": "passed",
"seconds": 3.98,
"summary": ".................... [100%]\n20 passed in 1.84s\n\n"
},
{
"batch": 17,
"files": [
"test_wordpress_baseline.py",
"test_worker_queue_v6.py",
"test_workspace_v810.py"
],
"status": "passed",
"seconds": 6.93,
"summary": "................ [100%]\n16 passed in 4.65s\n\n"
},
{
"batch": 18,
"files": [
"test_workspace_v812.py",
"test_wp_complex_v820.py",
"test_wp_migration_v81.py"
],
"status": "passed",
"seconds": 4.39,
"summary": "............................... [100%]\n31 passed in 2.21s\n\n"
},
{
"batch": 19,
"files": [
"test_wp_migration_v82.py",
"test_wp_migration_v83.py"
],
"status": "passed",
"seconds": 5.67,
"summary": ".................... [100%]\n20 passed in 3.53s\n\n"
}
],
"all_passed": true
}
File diff suppressed because one or more lines are too long
+18
View File
@@ -0,0 +1,18 @@
[
{
"device": "desktop",
"width": 1365,
"theme_body_found": 1,
"horizontal_overflow": false,
"script_errors": [],
"screenshot": "reports/v833/trade-pro-desktop.png"
},
{
"device": "phone",
"width": 390,
"theme_body_found": 1,
"horizontal_overflow": false,
"script_errors": [],
"screenshot": "reports/v833/trade-pro-phone.png"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because one or more lines are too long
+170
View File
@@ -0,0 +1,170 @@
[
{
"width": 1440,
"screenshot": "hesupipe-home-blocks-1440.png",
"facts": {
"bodyScroll": 1440,
"viewport": 1440,
"featuresOuter": {
"x": 0,
"width": 1440,
"display": "block",
"cols": "none"
},
"listOuter": {
"x": 0,
"width": 1440,
"display": "block",
"cols": "none"
},
"featuresInner": {
"x": 107,
"width": 1226,
"display": "grid",
"cols": "293px 293px 293px 293px"
},
"listInner": {
"x": 107,
"width": 1226,
"display": "grid",
"cols": "394.656px 394.672px 394.656px"
},
"featureCount": 4,
"productCount": 6,
"filterCount": 0,
"module": {
"x": 0,
"width": 1440,
"display": "block",
"cols": "none"
}
},
"script_errors": []
},
{
"width": 820,
"screenshot": "hesupipe-home-blocks-820.png",
"facts": {
"bodyScroll": 820,
"viewport": 820,
"featuresOuter": {
"x": 0,
"width": 820,
"display": "block",
"cols": "none"
},
"listOuter": {
"x": 0,
"width": 820,
"display": "block",
"cols": "none"
},
"featuresInner": {
"x": 24,
"width": 772,
"display": "grid",
"cols": "377px 377px"
},
"listInner": {
"x": 24,
"width": 772,
"display": "grid",
"cols": "375.5px 375.5px"
},
"featureCount": 4,
"productCount": 6,
"filterCount": 0,
"module": {
"x": 0,
"width": 820,
"display": "block",
"cols": "none"
}
},
"script_errors": []
},
{
"width": 390,
"screenshot": "hesupipe-home-blocks-390.png",
"facts": {
"bodyScroll": 390,
"viewport": 390,
"featuresOuter": {
"x": 0,
"width": 390,
"display": "block",
"cols": "none"
},
"listOuter": {
"x": 0,
"width": 390,
"display": "block",
"cols": "none"
},
"featuresInner": {
"x": 18,
"width": 354,
"display": "grid",
"cols": "354px"
},
"listInner": {
"x": 18,
"width": 354,
"display": "grid",
"cols": "354px"
},
"featureCount": 4,
"productCount": 6,
"filterCount": 0,
"module": {
"x": 0,
"width": 390,
"display": "block",
"cols": "none"
}
},
"script_errors": []
},
{
"width": 375,
"screenshot": "hesupipe-home-blocks-375.png",
"facts": {
"bodyScroll": 375,
"viewport": 375,
"featuresOuter": {
"x": 0,
"width": 375,
"display": "block",
"cols": "none"
},
"listOuter": {
"x": 0,
"width": 375,
"display": "block",
"cols": "none"
},
"featuresInner": {
"x": 18,
"width": 339,
"display": "grid",
"cols": "339px"
},
"listInner": {
"x": 18,
"width": 339,
"display": "grid",
"cols": "339px"
},
"featureCount": 4,
"productCount": 6,
"filterCount": 0,
"module": {
"x": 0,
"width": 375,
"display": "block",
"cols": "none"
}
},
"script_errors": []
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

+15
View File
@@ -0,0 +1,15 @@
{
"release": "8.33.1-dev.1",
"industrial": "1.3.1",
"python_total_passed": 439,
"regression_batches": 19,
"all_batches_passed": true,
"browser_viewports": [
1440,
820,
390,
375
],
"http_worker": "pass",
"caveat": "sample data; not production hesupipe.com"
}
+33
View File
@@ -0,0 +1,33 @@
"""Isolated Chromium smoke of shipped standalone theme comparison; no real server."""
import asyncio,json
from pathlib import Path
from playwright.async_api import async_playwright
ROOT=Path(__file__).resolve().parents[1];REPORT=ROOT/'reports/v833';REPORT.mkdir(parents=True,exist_ok=True)
async def main():
async with async_playwright() as p:
browser=await p.chromium.launch(headless=True,executable_path='/usr/bin/chromium',args=['--no-sandbox','--disable-dev-shm-usage'])
records=[]
for device,width in (('desktop',1365),('phone',390)):
page=await browser.new_page(viewport={'width':width,'height':850},device_scale_factor=1)
errs=[];page.on('pageerror',lambda e:errs.append(str(e)))
html=(ROOT/'examples/V8_33_五套主题真实页面_离线交互预览.html').read_text(encoding='utf-8')
await page.set_content(html,wait_until='load',timeout=60000)
await page.select_option('#theme','trade-pro')
await page.select_option('#page','product')
await page.frame_locator('#preview').locator('.sf-tp-product-hero').wait_for(state='visible',timeout=12000)
await page.wait_for_timeout(1600)
if device=='phone':await page.locator('[data-width="390px"]').click()
title=await page.title()
count=await page.frame_locator('#preview').locator('.pro-trade-pro').count()
overflow=await page.evaluate('document.documentElement.scrollWidth>window.innerWidth')
img=REPORT/f'trade-pro-{device}.png'
await page.screenshot(path=str(img),full_page=True)
assert count>0,(device,'theme body missing')
assert not overflow,(device,'overflow')
assert not errs,(device,errs)
records.append({'device':device,'width':width,'theme_body_found':count,'horizontal_overflow':overflow,'script_errors':errs,'screenshot':str(img.relative_to(ROOT))})
await page.close()
await browser.close()
(REPORT/'browser-results.json').write_text(json.dumps(records,ensure_ascii=False,indent=2),encoding='utf-8')
print('Chromium PASS:', records)
if __name__=='__main__':asyncio.run(main())
+46
View File
@@ -0,0 +1,46 @@
"""Chromium isolated visual check for industrial features and collection."""
import json
from pathlib import Path
from playwright.sync_api import sync_playwright
root=Path(__file__).resolve().parents[1]
pagehtml=(root/'examples/v8331-real-built-page.html').read_text()
dest=root/'reports/v8331';dest.mkdir(parents=True,exist_ok=True)
rows=[]
with sync_playwright() as p:
browser=p.chromium.launch(headless=True,executable_path='/usr/bin/chromium',args=['--no-sandbox','--disable-dev-shm-usage'])
for width in [1440,820,390,375]:
page=browser.new_page(viewport={'width':width,'height':920},device_scale_factor=1)
errors=[]
page.on('pageerror',lambda exc: errors.append(str(exc)))
page.set_content(pagehtml,wait_until='load',timeout=30000)
page.wait_for_timeout(150)
facts=page.evaluate('''() => {
const shell=document.querySelector('.pro-custom-content');
const outerFeature=document.querySelector('.sf-section.sf-features');
const outerList=document.querySelector('.sf-section.sf-collection');
const innerFeature=document.querySelector('.sf-section.sf-features .sf-container > .sf-features');
const innerList=document.querySelector('.sf-section.sf-collection .sf-container > .sf-collection');
const box=el=>el?({x:Math.round(el.getBoundingClientRect().x),width:Math.round(el.getBoundingClientRect().width),display:getComputedStyle(el).display,cols:getComputedStyle(el).gridTemplateColumns}):null;
return {bodyScroll:document.documentElement.scrollWidth,viewport:innerWidth,
featuresOuter:box(outerFeature),listOuter:box(outerList),featuresInner:box(innerFeature),listInner:box(innerList),
featureCount:document.querySelectorAll('.sf-feature').length,
productCount:document.querySelectorAll('.sf-item').length,
filterCount:document.querySelectorAll('.pro-custom-content .sf-filters').length,
module:box(shell)};
}''')
assert facts['featuresOuter']['display']=='block',facts
assert facts['listOuter']['display']=='block',facts
assert facts['featuresInner']['display']=='grid',facts
assert facts['listInner']['display']=='grid',facts
assert facts['featuresInner']['width']>=min(900,width-80),facts
assert facts['productCount']==6 and facts['featureCount']==4,facts
assert facts['filterCount']==0,facts
assert facts['bodyScroll']<=width+1,facts
assert not errors,errors
shot=dest/f'hesupipe-home-blocks-{width}.png'
page.locator('.pro-custom-content').screenshot(path=str(shot),timeout=30000)
rows.append({'width':width,'screenshot':shot.name,'facts':facts,'script_errors':errors})
page.close()
browser.close()
(dest/'browser-results.json').write_text(json.dumps(rows,ensure_ascii=False,indent=2))
for x in rows:print(x['width'], 'feature grid:',x['facts']['featuresInner']['cols'], 'product grid:', x['facts']['listInner']['cols'], 'filters:',x['facts']['filterCount'])
+62
View File
@@ -0,0 +1,62 @@
"""Make a fully offline industrial-theme homepage preview from the real static builder."""
import os,sys,json,html,re,tempfile
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
sys.path[:0]=[str(ROOT),str(ROOT/'tests')]
from tests.legacy import LegacyTestClient
from app import db,theme_store
from app.main import app
def generate():
target=ROOT/'examples'; target.mkdir(exist_ok=True)
with tempfile.TemporaryDirectory() as temporary:
db.DATA=Path(temporary);db.DB_FILE=db.DATA/'data.sqlite3';theme_store.THEME_ROOT=db.DATA/'theme_packages'
os.environ['SITEFORGE_ALLOW_LEGACY_DIRECT_DESIGN_WRITE']='1'
os.environ['SITEFORGE_ADMIN_PASSWORD']='demo-test-admin-secret-2026'
os.environ['SITEFORGE_SESSION_SECRET']='demo-secret-not-for-use-outside-test-2026'
with LegacyTestClient(app) as c:
assert c.post('/api/auth/login',json={'username':'admin','password':os.environ['SITEFORGE_ADMIN_PASSWORD']}).status_code==200
c.headers.update({'X-SiteForge-CSRF':c.get('/api/auth/me').json()['csrf']})
r=c.post('/api/sites',json={'name':'HESU PIPE · 排版验证样站','theme':'industrial','domain':'https://example.test','mode':'migration','tagline':'工业管道与工程材料 · 完整产品方案'})
assert r.status_code==201,r.text; sid=r.json()['id']
home=next(e for e in c.get(f'/api/sites/{sid}/entries').json() if e['route']=='/')
assert c.put(f'/api/factory/sites/{sid}/theme',json={'theme':'industrial','version':'1.3.1'}).status_code==200
products=[('HDPE 给水管','适用于市政给水及工业供水管网。'),('双壁波纹排水管','适用于市政雨污水管网和地下排水工程。'),('PVC-U 排水管','耐腐蚀、规格丰富的塑料管材系统。'),('PPR 冷热水管','建筑冷热水输送常用管道系列。'),('钢丝网骨架复合管','适用于承压输水与工程管线系统。'),('HDPE 管道连接件','用于热熔及电熔连接的各类管件。')]
for i,(title,summary) in enumerate(products,1):
res=c.post(f'/api/sites/{sid}/entries',json={'type':'product','title':title,'route':f'/products/pipe-{i}/','summary':summary})
assert res.status_code==201,res.text
blocks=[{'id':'features-hp-v8331','type':'features','title':'核心优势','items':[
{'title':'管道产品供应','text':'覆盖多种工程管材和配件,便于项目集中采购。'},
{'title':'规格与质量管理','text':'根据实际应用提供产品选型和规格资料。'},
{'title':'工程应用支持','text':'面向供水、排水及工业管网项目的应用场景。'},
{'title':'全球交付服务','text':'从订单沟通到物流交付,提供清晰的合作流程。'},
]}, {'id':'collection-hp-v8331','type':'collection','title':'动态内容列表 · 精选产品',
'subtitle':'PRODUCT COLLECTION','source':'product','limit':6,'columns':3,
'href':'/products/','button':'查看全部产品'}]
saved=c.put(f'/api/sites/{sid}/pages/{home["id"]}/blueprint',json={'blocks':blocks,'page_template':'home'})
assert saved.status_code==200,saved.text
built=c.post(f'/api/sites/{sid}/builds',json={})
assert built.status_code==200,built.text
bid=built.json()['version']; dist=db.DATA/'builds'/sid/bid/'dist'
assert (dist/'index.html').exists(),dist
source=(dist/'index.html').read_text()
# Inline exact packaged CSS/JS, so the demo is readable without a server or external requests.
def stylesheet(m):
path=m.group(1)
local=dist/path
return '<style>\n'+local.read_text(encoding='utf-8')+'\n</style>' if local.is_file() else ''
source=re.sub(r'<link rel="stylesheet" href="(assets/[A-Za-z0-9._-]+\.css)">',stylesheet,source)
def script(m):
local=dist/m.group(1)
if local.is_file():
return '<script>\n'+local.read_text(encoding='utf-8').replace('</script','<\\/script')+'\n</script>'
return ''
source=re.sub(r'<script defer src="(assets/[A-Za-z0-9._-]+\.js)"></script>',script,source)
# These paths are demonstrative and intentionally not published.
isolated=target/'V8_33_1_HesuPipe_首页布局真实构建预览.html'
snippet=json.dumps(source,ensure_ascii=False).replace('<','\\u003c')
wrapper='''<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>HesuPipe · 首页核心优势与产品列表</title><style>*{box-sizing:border-box}body{margin:0;background:#eef2f5;color:#1d3344;font-family:system-ui,-apple-system,sans-serif}.bar{display:flex;gap:15px;align-items:center;justify-content:space-between;padding:14px 23px;background:#fff;border-bottom:1px solid #dce4e9;flex-wrap:wrap}.bar strong{font-size:16px}.bar small{display:block;font-size:11px;color:#657a89;margin-top:4px}.buttons{display:flex;gap:6px}.buttons button{border:1px solid #cbdce4;background:#f6f9fa;padding:9px 14px;border-radius:6px;font-weight:650;color:#1e4359;cursor:pointer}.buttons button[aria-pressed=true]{background:#0f5e78;color:#fff}iframe{border:0;width:100%;height:calc(100vh - 74px);min-height:550px;background:#fff;display:block;margin:auto;transition:width .15s}.note{position:fixed;right:20px;bottom:18px;border:1px solid #d0dce3;border-radius:9px;background:#fff;padding:10px 13px;font-size:12px;box-shadow:0 4px 15px #0001}</style></head><body><div class="bar"><div><strong>HESU PIPE · 核心优势 / 动态内容列表</strong><small>V8.33.1 从实际静态构建生成的离线页面 · 示范文字与占位产品图</small></div><div class="buttons"><button data-size="100%" aria-pressed="true">电脑版</button><button data-size="820px" aria-pressed="false">平板</button><button data-size="390px" aria-pressed="false">手机版</button></div></div><iframe title="工业建材主题首页实际构建预览" sandbox="allow-scripts" id="preview"></iframe><span class="note">仅检查预览 · 不连接线上网站</span><script id="payload" type="application/json">PAYLOAD</script><script>const raw=JSON.parse(document.getElementById('payload').textContent);const target=document.getElementById('preview');target.srcdoc=raw;document.querySelectorAll('[data-size]').forEach(btn=>btn.addEventListener('click',()=>{target.style.width=btn.dataset.size;document.querySelectorAll('[data-size]').forEach(item=>item.setAttribute('aria-pressed',String(item===btn)));}));</script></body></html>'''
isolated.write_text(wrapper.replace('PAYLOAD',snippet),encoding='utf-8')
(target/'v8331-real-built-page.html').write_text(source,encoding='utf-8')
print('generated',isolated,'bytes',isolated.stat().st_size,'build',bid)
if __name__=='__main__':generate()
+78
View File
@@ -0,0 +1,78 @@
"""End-to-end HTTP + Worker smoke test on an isolated disposable SQLite database.
Launches an actual uvicorn process, calls authenticated HTTP APIs, runs a real
Worker claim + isolated subprocess build, then downloads and checks the archive.
Never points at production data or remote deployment services.
"""
from __future__ import annotations
import io
import json
import os
import socket
import subprocess
import sys
import tempfile
import time
import zipfile
from pathlib import Path
from urllib.error import URLError
from urllib.request import urlopen
import httpx
ROOT=Path(__file__).resolve().parent.parent
def run():
with tempfile.TemporaryDirectory(prefix='siteforge-e2e-') as folder:
with socket.socket() as sock:
sock.bind(('127.0.0.1',0)); port=sock.getsockname()[1]
base=f'http://127.0.0.1:{port}'
env=os.environ.copy()
env.update(SITEFORGE_ADMIN_PASSWORD='e2e-only-dummy-password-123456',
SITEFORGE_SESSION_SECRET='e2e-only-session-secret-long-value-123456',
SITEFORGE_DATA_DIR=folder,SITEFORGE_DB_FILE=str(Path(folder)/'siteforge.sqlite3'),
SITEFORGE_DATABASE_URL='',SITEFORGE_REDIS_URL='',SITEFORGE_COOKIE_SECURE='false')
proc=subprocess.Popen([sys.executable,'-m','uvicorn','app.main:app','--host','127.0.0.1','--port',str(port)],
cwd=ROOT,env=env,stdout=subprocess.DEVNULL,stderr=subprocess.PIPE)
try:
for n in range(120):
if proc.poll() is not None:raise RuntimeError('Web startup failed: '+proc.stderr.read().decode(errors='replace')[-3000:])
try:
with urlopen(base+'/api/ready',timeout=0.5) as result:
if result.status==200:break
except (OSError,URLError):time.sleep(0.1)
else:raise RuntimeError('Web readiness timeout')
with httpx.Client(base_url=base,timeout=20,follow_redirects=False) as client:
auth=client.post('/api/auth/login',json={'username':'admin','password':env['SITEFORGE_ADMIN_PASSWORD']})
assert auth.status_code==200,auth.text
me=client.get('/api/auth/me')
assert me.status_code==200,me.text
client.headers['X-SiteForge-CSRF']=me.json()['csrf']
site=client.post('/api/sites',json={'name':'V8.33 Trade Pro Worker','theme':'trade-pro'})
assert site.status_code==201,site.text
sid=site.json()['id']
page=client.post(f'/api/sites/{sid}/entries',json={'type':'page','title':'Test about','route':'/about/','body':'E2E smoke content'})
assert page.status_code==201,page.text
job=client.post(f'/api/sites/{sid}/builds',json={'mirror_images':False},headers={'Idempotency-Key':'e2e-build-000001'})
assert job.status_code==202,job.text
completed=subprocess.run([sys.executable,'-c','from app import db,enterprise,worker; db.init(); enterprise.setup(); assert worker.run_once()'],cwd=ROOT,env=env,capture_output=True,text=True,timeout=70)
assert completed.returncode==0,completed.stderr[-2500:]
builds=client.get(f'/api/sites/{sid}/builds')
assert builds.status_code==200,builds.text
done=[x for x in builds.json() if x['status']=='complete']
if not done:
raise AssertionError('No completed static build: '+str(builds.json())[:1200])
archive=client.get(f"/api/sites/{sid}/builds/{done[0]['id']}/download")
assert archive.status_code==200,archive.text[:300]
with zipfile.ZipFile(io.BytesIO(archive.content)) as z:
assert 'index.html' in z.namelist()
assert 'about/index.html' in z.namelist()
assert '<html lang="en"' in z.read('index.html').decode('utf-8')
preview=client.get(f"/preview/{sid}/{done[0]['id']}/about/")
assert preview.status_code==200 and 'E2E smoke content' in preview.text
print('PASS: ready -> login/CSRF -> create site/page -> enqueue -> Worker build -> ZIP -> preview')
finally:
proc.terminate()
try:proc.communicate(timeout=5)
except subprocess.TimeoutExpired:proc.kill();proc.communicate()
if __name__=='__main__':run()
+28
View File
@@ -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/v833-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.33.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())
+2 -2
View File
@@ -6,7 +6,7 @@ import sys
ROOT=Path(__file__).resolve().parent.parent ROOT=Path(__file__).resolve().parent.parent
sys.path.insert(0,str(ROOT)) sys.path.insert(0,str(ROOT))
from app.version import VERSION from app.version import VERSION
assert VERSION=='8.32.0-dev.1' assert VERSION=='8.33.1-dev.1'
api=(ROOT/'app'/'main.py').read_text(encoding='utf-8') 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 '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 assert "@app.post('/api/sites/{site_id}/builds', status_code=202)" in api
@@ -22,4 +22,4 @@ for file in (ROOT/'requirements.lock',ROOT/'requirements-dev.lock'):
assert (ROOT/'.github/workflows/ci.yml').is_file() assert (ROOT/'.github/workflows/ci.yml').is_file()
for file in (ROOT/'app/static').glob('*.js'): for file in (ROOT/'app/static').glob('*.js'):
subprocess.run(['node','--check',str(file)],check=True) subprocess.run(['node','--check',str(file)],check=True)
print('PASS: version, 4× themes, worker route, pinned dependencies, CI workflow, JS syntax') print('PASS: version, 5× themes, worker route, pinned dependencies, CI workflow, JS syntax')
+80
View File
@@ -0,0 +1,80 @@
"""V8.33.1: industrial/hesupipe advantage and dynamic-card regression."""
import json
from pathlib import Path
from bs4 import BeautifulSoup
from app import blueprints, theme_store
from test_factory_v2 import factory, site
def _blocks():
return [
{'id':'benefits-1234','type':'features','title':'我们的核心优势','items':[
{'title':'专业管道解决方案','text':'支持项目选型、施工方案和规格咨询。'},
{'title':'严格质量控制','text':'规格清晰,支持检验与资料交付。'},
{'title':'丰富的项目经验','text':'多种应用场景和工程项目。'},
{'title':'国际供应能力','text':'支持国际物流和技术服务。'}]},
{'id':'productss-1234','type':'collection','title':'热销管道产品','subtitle':'PRODUCT SOLUTIONS',
'source':'product','limit':6,'columns':3,'button':'浏览所有产品','href':'/products/'},
]
def test_features_four_cards_form_balanced_rows():
markup=blueprints.render_blocks(_blocks()[:1],{}, {'route':'/','type':'page'}, [])
assert 'style="--sf-feature-cols:4"' in markup
soup=BeautifulSoup(markup,'html.parser')
assert len(soup.select('.sf-features .sf-feature'))==4
assert all(x.select_one('h3') for x in soup.select('.sf-feature'))
def test_home_dynamic_list_no_filters_and_listing_filter_present():
rows=[{'route':'/products/p'+str(i)+'/','title':'测试管件 '+str(i),'type':'product','cover':'',
'summary':'HDPE 工程管材','meta_json':'{}'} for i in range(12)]
home=blueprints.render_blocks(_blocks()[1:],{}, {'route':'/','type':'page'},rows)
assert 'data-sf-filterable="false"' in home
assert home.count('class="sf-item"')==6
listing=blueprints.render_blocks([dict(_blocks()[1],limit=12)],{}, {'route':'/products/','type':'page'},rows)
assert 'data-sf-filterable="true"' in listing
assert listing.count('class="sf-item"')==12
def test_industrial_new_css_and_version_archive():
theme=theme_store.resolve('industrial')
assert theme_store.latest('industrial')=='1.3.1'
assert theme_store.resolve('industrial','1.3.0')!=theme
css=(theme/'blocks.css').read_text()
assert '.sf-theme-industrial .pro-custom-content .sf-section.sf-features > .sf-container > .sf-features' in css
assert 'object-fit:contain' in css
assert '.sf-section.sf-collection,.sf-section.sf-stats' in (Path(__file__).parents[1]/'app/static/blocks.css').read_text()
assert not (theme_store.resolve('industrial','1.3.0')/'blocks.css').exists()
def test_hesupipe_real_builder_preview_and_archive(factory):
sid=site(factory)
assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'industrial','version':'1.3.1'}).status_code==200
home=next(x for x in factory.get(f'/api/sites/{sid}/entries').json() if x['route']=='/')
for i in range(1,7):
item=factory.post(f'/api/sites/{sid}/entries',json={
'type':'product','title':f'HDPE 管道产品 {i}','route':f'/products/hdpe-{i}/',
'summary':'适用于市政供水和工业管线的专业产品'})
assert item.status_code==201,item.text
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']
html=factory.get(f'/preview/{sid}/{version}/').text
soup=BeautifulSoup(html,'html.parser')
assert soup.select_one('.sf-theme-industrial .pro-custom-content .sf-features[style*="--sf-feature-cols:4"]')
assert len(soup.select('.pro-custom-content .sf-collection .sf-item'))==6
assert soup.select_one('.pro-custom-content .sf-collection[data-sf-filterable]').get('data-sf-filterable')=='false'
styles=factory.get(f'/preview/{sid}/{version}/assets/blocks.css').text
assert '.sf-theme-industrial .pro-custom-content .sf-item-img' in styles
js=factory.get(f'/preview/{sid}/{version}/assets/site-runtime.js').text
assert 'data-sf-filterable' in js
report=factory.get(f'/api/sites/{sid}/builds/{version}/report').json()
assert not report['errors']
assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'industrial','version':'1.3.0'}).status_code==200
older=factory.post(f'/api/sites/{sid}/builds',json={})
assert older.status_code==200,older.text
oldcss=factory.get(f'/preview/{sid}/{older.json()["version"]}/assets/blocks.css').text
assert '.sf-theme-industrial .pro-custom-content .sf-item-img' not in oldcss
+1 -1
View File
@@ -11,7 +11,7 @@ ROOT=Path(__file__).resolve().parents[1]
def test_all_themes_preserve_v110_and_publish_v120(factory): def test_all_themes_preserve_v110_and_publish_v120(factory):
for theme in THEMES: for theme in THEMES:
assert theme_store.latest(theme)==('1.4.1' if theme=='automotive' else '1.3.0') assert theme_store.latest(theme)==('1.4.1' if theme=='automotive' else '1.3.1' if theme=='industrial' else '1.3.0')
assert theme_store.resolve(theme,'1.1.0').is_dir() assert theme_store.resolve(theme,'1.1.0').is_dir()
assert theme_store.compatibility(theme,'1.1.0')['valid'] assert theme_store.compatibility(theme,'1.1.0')['valid']
assert theme_store.compatibility(theme,'1.2.0')['valid'] assert theme_store.compatibility(theme,'1.2.0')['valid']
+1 -1
View File
@@ -11,7 +11,7 @@ KINDS=('home','listing','product','article','about','contact')
def test_new_and_legacy_page_layouts_are_version_isolated(factory): def test_new_and_legacy_page_layouts_are_version_isolated(factory):
for name in THEMES: for name in THEMES:
assert theme_store.latest(name)==('1.4.1' if name=='automotive' else '1.3.0') assert theme_store.latest(name)==('1.4.1' if name=='automotive' else '1.3.1' if name=='industrial' else '1.3.0')
assert theme_store.compatibility(name,'1.2.0')['valid'] assert theme_store.compatibility(name,'1.2.0')['valid']
assert theme_store.compatibility(name,'1.3.0')['valid'] assert theme_store.compatibility(name,'1.3.0')['valid']
old=theme_store.resolve(name,'1.2.0') old=theme_store.resolve(name,'1.2.0')
+2 -1
View File
@@ -16,7 +16,8 @@ def test_automotive_new_and_old_versions_remain_available(factory):
assert theme_store.compatibility('automotive',version)['valid'] assert theme_store.compatibility('automotive',version)['valid']
assert ('auto31-home' in (p/'pages/home.html').read_text()) == (version == '1.4.0') assert ('auto31-home' in (p/'pages/home.html').read_text()) == (version == '1.4.0')
assert "'pages/' ~ kind" in (p/'pro.html').read_text() assert "'pages/' ~ kind" in (p/'pro.html').read_text()
assert theme_store.latest('industrial')=='1.3.0' assert theme_store.latest('industrial')=='1.3.1'
assert theme_store.resolve('industrial','1.3.0') != theme_store.resolve('industrial')
assert 'auto31-home' in (theme_store.resolve('automotive','1.4.0')/'pages/home.html').read_text() assert 'auto31-home' in (theme_store.resolve('automotive','1.4.0')/'pages/home.html').read_text()
+76
View File
@@ -0,0 +1,76 @@
"""V8.33 five-theme, preview parity and business-safe migration checks."""
import json
from pathlib import Path
from test_factory_v2 import factory,site
from app import core, theme_store, theme_pro
ROOT=Path(__file__).resolve().parents[1]
def test_registry_includes_five_themes_and_old_manifests():
assert set(core.THEMES)==set(theme_pro.THEMES)==set(theme_store.BUILTIN)
assert len(core.THEMES)==5
assert 'trade-pro' in core.THEMES
for name in ('automotive','industrial','corporate','minimal'):
assert theme_store.resolve(name,'1.1.0').is_dir()
assert theme_store.compatibility('trade-pro')['valid']
def test_theme_gallery_and_editor_offer_trade_pro(factory):
gallery=factory.get('/theme-gallery')
assert gallery.status_code==200
assert '<option value="trade-pro">' in gallery.text
assert "'trade-pro'" in gallery.text
sid=site(factory)
editor=factory.get(f'/studio/{sid}')
assert editor.status_code in (200, 404)
src=(ROOT/'app/templates/editor.html').read_text(encoding='utf-8')
assert '<option value="trade-pro">' in src
assert 'sandbox="allow-scripts"' in src
assert 'allow-same-origin' not in src
def test_five_theme_preview_styles_and_frame_security(factory):
for name in core.THEMES:
response=factory.get(f'/api/factory/themes/{name}/preview',params={'page_type':'home'})
assert response.status_code==200,(name,response.text[:200])
assert 'text/html' in response.headers.get('content-type','')
assert response.headers['x-frame-options']=='SAMEORIGIN'
assert 'preview.blocks.css' in response.text or (name=='automotive' and 'preview.bundle.css' in response.text)
stylesheet=factory.get(f'/api/factory/themes/{name}/preview.blocks.css')
assert stylesheet.status_code==200
assert 'text/css' in stylesheet.headers.get('content-type','')
src=(ROOT/'themes/trade-pro/blocks.css').read_text(encoding='utf-8')
assert '.sf-theme-trade-pro' in src
def test_theme_preview_several_real_page_types(factory):
for kind in ('listing','product','about','contact','brand','article','faq'):
res=factory.get('/api/factory/themes/trade-pro/preview',params={'page_type':kind})
assert res.status_code==200,(kind,res.text[:250])
assert 'pro-trade-pro' in res.text
assert 'lang="en"' in res.text
def test_trade_pro_preserves_original_routes_and_pinned_build(factory):
sid=site(factory)
path='/product/exact-original-slug/'
created=factory.post(f'/api/sites/{sid}/entries',json={'type':'product','title':'Hydraulic Pump','route':path,'seo_title':'Precise SEO Title','summary':'Export pump'})
assert created.status_code in (200,201)
changed=factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'trade-pro','version':'1.0.0'})
assert changed.status_code==200
result=factory.post(f'/api/sites/{sid}/builds',json={})
assert result.status_code==200,result.text
bid=result.json()['version']
page=factory.get(f'/preview/{sid}/{bid}{path}')
assert page.status_code==200
assert '<html lang="en"' in page.text
assert 'Precise SEO Title' in page.text
assert factory.get(f'/preview/{sid}/{bid}/assets/blocks.css').status_code==200
assert theme_store.pinned(sid)['version']=='1.0.0'
def test_trade_pro_defensive_preview_and_runtime_css(factory):
res=factory.get('/api/factory/themes/trade-pro/preview',params={'page_type':'unknown'})
assert res.status_code==400
res=factory.get('/api/factory/themes/not-here/preview.blocks.css')
assert res.status_code==404
+86
View File
@@ -0,0 +1,86 @@
/* Portable SiteForge blocks, loaded with every theme (all styles scoped to sf-). */
.sf-section{background:var(--sf-bg,transparent);color:var(--sf-fg,inherit);padding:var(--sf-pad,82px) 0;margin:0}.sf-section:nth-of-type(even){background:var(--sf-bg,#f4f6f9)}.sf-container{max-width:1200px;margin:auto;padding:0 32px}.sf-section h1,.sf-section h2,.sf-section h3,.sf-section p{color:inherit;line-height:1.3}.sf-section h1{font-size:clamp(38px,5vw,76px);font-weight:790;letter-spacing:-.052em}.sf-section h2{font-size:clamp(27px,3.2vw,44px);font-weight:760;letter-spacing:-.037em}.sf-kicker{display:block;font-size:12px;letter-spacing:.17em;text-transform:uppercase;font-weight:740;color:var(--accent,#3569d3);margin-bottom:18px}.sf-hero-grid,.sf-image-text{display:grid;grid-template-columns:1.08fr .92fr;gap:68px;align-items:center}.sf-hero-copy p{font-size:18px;line-height:1.75;max-width:520px;opacity:.75}.sf-hero-img,.sf-image-side{overflow:hidden;border-radius:9px;min-height:380px;background:linear-gradient(130deg,#d5e1ee,#eef3f9);display:grid;place-items:center}.sf-hero-img img,.sf-image-side img{width:100%;height:100%;max-height:580px;object-fit:cover}.sf-hero-abstract{font-size:110px;color:#a7bdd4}.sf-button{display:inline-flex;align-items:center;justify-content:center;gap:28px;background:var(--accent,#315ee2);color:white!important;padding:16px 26px;border-radius:6px;font-size:14px;text-decoration:none;font-weight:720;margin-top:24px}.sf-section-head{display:flex;align-items:end;justify-content:space-between;margin-bottom:32px;gap:24px}.sf-section-head h2{margin:0}.sf-section-head>a,.sf-link{color:var(--accent,#3569d3);font-weight:700;text-decoration:none}.sf-prose{font-size:16px;line-height:1.85;max-width:930px}.sf-prose p{line-height:1.85}.sf-features,.sf-collection,.sf-stats{display:grid;gap:20px}.sf-features{grid-template-columns:repeat(3,minmax(0,1fr))}.sf-feature{border:1px solid #e0e6eb;padding:32px 28px;border-radius:8px;background:#fff;color:#253247}.sf-feature>span{font-size:12px;font-weight:800;color:var(--accent,#346add)}.sf-feature h3{font-size:22px;margin:30px 0 8px}.sf-feature p{font-size:14px;line-height:1.7;color:#647181}.sf-stats{grid-template-columns:repeat(4,1fr);padding:40px 0;border-top:1px solid #b7c0cd66;border-bottom:1px solid #b7c0cd66}.sf-stats>div{display:flex;flex-direction:column;gap:8px}.sf-stats strong{font-size:clamp(28px,4vw,53px);letter-spacing:-.04em}.sf-stats span{font-size:12px;opacity:.65;letter-spacing:.04em}.sf-collection{grid-template-columns:repeat(var(--sf-cols,3),minmax(0,1fr))}.sf-item{display:block;text-decoration:none;color:#233044;background:#fff;border:1px solid #e8edf1;border-radius:8px;overflow:hidden;transition:transform .2s,box-shadow .2s}.sf-item:hover{transform:translateY(-4px);box-shadow:0 18px 38px #344d7018}.sf-item-img{height:210px;overflow:hidden;background:#e4eaf0}.sf-item-img img{height:100%;width:100%;object-fit:cover}.sf-placeholder{display:grid;place-items:center;width:100%;height:100%;min-height:190px;background:linear-gradient(130deg,#e2eaf2,#c9d7e6);font-size:72px;color:#8399b2;font-weight:800}.sf-large{min-height:360px}.sf-item-copy{padding:25px}.sf-item-copy small{text-transform:uppercase;letter-spacing:.13em;color:var(--accent,#376edc)}.sf-item-copy h3{font-size:22px;margin:12px 0 7px}.sf-item-copy p{font-size:14px;line-height:1.6;color:#7a8796;min-height:42px}.sf-item-copy span{font-size:12px;color:var(--accent,#376edc);font-weight:800}.sf-image-text>div>p{font-size:17px;line-height:1.8;max-width:520px;opacity:.7}.sf-reverse>div:first-child{order:2}.sf-gallery{display:grid;grid-template-columns:repeat(3,1fr);gap:15px}.sf-gallery img{width:100%;height:230px;object-fit:cover;border-radius:6px}.sf-faq{max-width:920px}.sf-faq details{border-bottom:1px solid #dce3eb;padding:22px 0}.sf-faq summary{font-size:17px;cursor:pointer;font-weight:700}.sf-faq p{color:#647181;line-height:1.75}.sf-cta-wrap{display:flex;justify-content:space-between;align-items:center;gap:24px}.sf-cta-wrap h2{margin:0}.sf-cta-wrap p{font-size:16px;opacity:.75}.sf-section a{cursor:pointer}.sf-muted{opacity:.65}
@media(max-width:820px){.sf-container{padding:0 20px}.sf-section{padding:50px 0}.sf-hero-grid,.sf-image-text{grid-template-columns:1fr;gap:26px}.sf-reverse>div:first-child{order:initial}.sf-hero-img,.sf-image-side{min-height:250px}.sf-features,.sf-collection{grid-template-columns:repeat(2,minmax(0,1fr))}.sf-stats{grid-template-columns:repeat(2,1fr)}.sf-cta-wrap{align-items:flex-start;flex-direction:column}}
@media(max-width:540px){.sf-features,.sf-collection,.sf-gallery{grid-template-columns:1fr}.sf-section-head{display:block}.sf-section-head>a{display:block;margin-top:13px}.sf-item-img{height:220px}.sf-hero-grid{gap:22px}.sf-hero-copy p{font-size:16px}.sf-stats{gap:28px}}
.sf-attributes{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 32px;border-top:1px solid #e5eaf0}.sf-attribute{display:flex;justify-content:space-between;align-items:center;gap:18px;border-bottom:1px solid #e5eaf0;padding:19px 10px;font-size:14px}.sf-attribute span{color:#748497}.sf-attribute strong{font-weight:720;text-align:right}@media(max-width:640px){.sf-attributes{grid-template-columns:1fr}}
/* SiteForge responsive no-code grid container */
.sf-layout-grid{display:grid;grid-template-columns:var(--sf-layout-cols,1fr 1fr);gap:var(--sf-layout-gap,24px);align-items:stretch}
.sf-layout-column{min-width:0}.sf-layout-column>.sf-section{padding:0;margin:0 0 18px}.sf-layout-column .sf-container{width:100%;padding:0}
.sf-layout-column>.sf-section:last-child{margin-bottom:0}
@media(max-width:820px){.sf-layout-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}}
@media(max-width:540px){.sf-layout-grid{grid-template-columns:minmax(0,1fr)!important}}
/* V8.6: accessible, script-free commercial design blocks */
.sf-slides{display:flex;gap:18px;overflow-x:auto;scroll-snap-type:x mandatory;padding:6px 0 18px;scrollbar-width:thin}
.sf-slide{flex:0 0 min(78%,720px);min-width:220px;margin:0;scroll-snap-align:start}
.sf-slide img{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:10px}
.sf-slide figcaption{padding:10px 2px;opacity:.7;font-size:14px}
.sf-video{aspect-ratio:16/9;max-width:1000px;background:#ebf0f5;border-radius:10px;overflow:hidden}
.sf-video iframe{width:100%;height:100%;border:0}
.sf-tab{padding:19px 0;border-bottom:1px solid #dde4ec;max-width:960px}
.sf-tab summary{font-size:18px;font-weight:700;cursor:pointer}.sf-tab p{max-width:850px;line-height:1.8}
.sf-prices{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:18px}
.sf-price{padding:28px;background:#fff;border:1px solid #dee5ef;border-radius:10px;color:#29394b}
.sf-price strong{display:block;font-size:27px;margin:14px 0}.sf-price li{margin:11px 0}.sf-price ul{padding-left:19px}
.sf-map-address{font-style:normal;font-size:18px;white-space:pre-line;line-height:1.8}
@media (prefers-reduced-motion:reduce){.sf-slides{scroll-behavior:auto}.sf-item{transition:none}}
@media(max-width:540px){.sf-slide{flex-basis:92%}}
/* V8.33.1 grid-class collision fix: block section and inner collection/features
share a class. The section must NOT become a grid container itself.
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.33.1 HesuPipe / Industrial scoped homepage blocks.
Deliberately affects only current industrial theme, not the other four.
Versioned copies remain unchanged. */
.sf-theme-industrial .pro-custom-content{min-width:0}
.sf-theme-industrial .pro-custom-content .sf-section{padding:clamp(54px,6vw,92px) 0}
.sf-theme-industrial .pro-custom-content .sf-container{max-width:var(--pro-wide,1270px);padding-inline:32px}
.sf-theme-industrial .pro-custom-content .sf-section-head{align-items:flex-end;margin-bottom:30px;gap:16px}
.sf-theme-industrial .pro-custom-content .sf-section-head h2{max-width:900px;font-size:clamp(28px,3.1vw,43px);line-height:1.18;letter-spacing:-.035em}
.sf-theme-industrial .pro-custom-content .sf-section-head>a{font-size:14px;white-space:nowrap;color:var(--pro-accent,#087c94)}
/* Advantage cards: evenly distribute 1–4 cards, maintain equal height, no orphan in 4-item groups. */
.sf-theme-industrial .pro-custom-content .sf-section.sf-features > .sf-container > .sf-features{display:grid;grid-template-columns:repeat(var(--sf-feature-cols,3),minmax(0,1fr));gap:18px}
.sf-theme-industrial .pro-custom-content .sf-feature{min-width:0;min-height:220px;border:1px solid #d9e5ea;border-top:3px solid var(--pro-accent,#0f7895);border-radius:10px;padding:28px 25px;background:#fff;display:flex;flex-direction:column;gap:0;box-shadow:0 8px 28px rgba(23,56,68,.035);overflow-wrap:anywhere}
.sf-theme-industrial .pro-custom-content .sf-feature>span{font-size:12px;font-weight:800;letter-spacing:.15em;color:var(--pro-accent,#0f7895)}
.sf-theme-industrial .pro-custom-content .sf-feature h3{font-size:clamp(18px,1.6vw,22px);margin:28px 0 10px;line-height:1.3;color:#143c4e}
.sf-theme-industrial .pro-custom-content .sf-feature p{font-size:14px;line-height:1.75;color:#56717e;margin:0}
/* Product gallery: stable image ratio, neat text baselines and equal card heights. */
.sf-theme-industrial .pro-custom-content .sf-section.sf-collection > .sf-container > .sf-collection{display:grid;grid-template-columns:repeat(var(--sf-cols,3),minmax(0,1fr));gap:21px;align-items:stretch}
.sf-theme-industrial .pro-custom-content .sf-item{min-width:0;height:100%;display:flex;flex-direction:column;border:1px solid #dfe8ec;border-radius:10px;background:#fff;overflow:hidden;color:#16384b;box-shadow:0 6px 18px rgba(16,59,82,.04)}
.sf-theme-industrial .pro-custom-content .sf-item-img{height:auto;aspect-ratio:4/3;min-height:0;background:#eff5f7;display:grid;place-items:center;padding:14px;overflow:hidden}
.sf-theme-industrial .pro-custom-content .sf-item-img img{display:block;width:100%;height:100%;object-fit:contain}
.sf-theme-industrial .pro-custom-content .sf-item-img .sf-placeholder{min-height:0;font-size:clamp(24px,5vw,52px)}
.sf-theme-industrial .pro-custom-content .sf-item-copy{padding:20px 22px 24px;display:flex;flex-direction:column;flex:1;min-width:0}
.sf-theme-industrial .pro-custom-content .sf-item-copy small{font-size:11px;font-weight:750;color:var(--pro-accent,#087c94);letter-spacing:.08em}
.sf-theme-industrial .pro-custom-content .sf-item-copy h3{margin:11px 0 8px;font-size:clamp(17px,1.5vw,21px);line-height:1.35;overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.sf-theme-industrial .pro-custom-content .sf-item-copy p{margin:0 0 15px;font-size:13px;line-height:1.65;color:#637f8c;min-height:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.sf-theme-industrial .pro-custom-content .sf-item-copy>span{margin-top:auto;padding-top:10px;font-weight:750;color:var(--pro-accent,#087c94)}
.sf-theme-industrial .pro-custom-content .sf-section.sf-collection > .sf-container > .sf-collection>.sf-muted{grid-column:1/-1;border:1px dashed #bdcdd4;border-radius:9px;padding:28px;text-align:center;background:#f6fafb;opacity:1;color:#617985}
/* The dedicated listing / category view retains filters. Homepage highlights do not. */
.sf-theme-industrial .pro-custom-content .sf-filters{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin:0 0 22px}
.sf-theme-industrial .pro-custom-content .sf-filters input,.sf-theme-industrial .pro-custom-content .sf-filters select{min-width:0;max-width:100%;border:1px solid #cbdbe3;border-radius:7px;background:#fff;color:#1b4055;padding:10px 13px;font-size:14px}
@media(max-width:960px){
.sf-theme-industrial .pro-custom-content .sf-section.sf-features > .sf-container > .sf-features{grid-template-columns:repeat(2,minmax(0,1fr))}
.sf-theme-industrial .pro-custom-content .sf-section.sf-collection > .sf-container > .sf-collection{grid-template-columns:repeat(2,minmax(0,1fr))}
.sf-theme-industrial .pro-custom-content .sf-container{padding-inline:24px}
}
@media(max-width:600px){
.sf-theme-industrial .pro-custom-content .sf-section{padding:45px 0}
.sf-theme-industrial .pro-custom-content .sf-container{padding-inline:18px}
.sf-theme-industrial .pro-custom-content .sf-section-head{align-items:flex-start;flex-direction:column;gap:10px;margin-bottom:20px}
.sf-theme-industrial .pro-custom-content .sf-section-head>a{white-space:normal;margin-top:0}
.sf-theme-industrial .pro-custom-content .sf-section.sf-features > .sf-container > .sf-features{grid-template-columns:minmax(0,1fr);gap:12px}
.sf-theme-industrial .pro-custom-content .sf-feature{min-height:0;padding:24px}
.sf-theme-industrial .pro-custom-content .sf-feature h3{margin-top:18px}
.sf-theme-industrial .pro-custom-content .sf-section.sf-collection > .sf-container > .sf-collection{grid-template-columns:minmax(0,1fr);gap:16px}
.sf-theme-industrial .pro-custom-content .sf-item-img{aspect-ratio:5/3}
.sf-theme-industrial .pro-custom-content .sf-item-copy{padding:17px 20px 21px}
.sf-theme-industrial .pro-custom-content .sf-filters{display:grid;grid-template-columns:minmax(0,1fr)}
.sf-theme-industrial .pro-custom-content .sf-filters input,.sf-theme-industrial .pro-custom-content .sf-filters select{width:100%}
}
@media(prefers-reduced-motion:reduce){.sf-theme-industrial .pro-custom-content .sf-item{transition:none}}
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"id": "industrial", "id": "industrial",
"name": "工业制造", "name": "工业制造",
"description": "V8.30 专业内页设计升级:行业化首页/产品/文章/企业/联系布局;保留抽屉菜单和可选 Tabs,历史版本独立可用。", "description": "V8.33.1 工业建材首页区块视觉修复:核心优势、动态列表自适应布局;版本1.3.0独立归档。",
"version": "1.3.0", "version": "1.3.1",
"style": "technical", "style": "technical",
"entry": "pro.html", "entry": "pro.html",
"styles": [ "styles": [
@@ -0,0 +1,54 @@
{
"id": "industrial",
"name": "工业制造",
"description": "V8.30 专业内页设计升级:行业化首页/产品/文章/企业/联系布局;保留抽屉菜单和可选 Tabs,历史版本独立可用。",
"version": "1.3.0",
"style": "technical",
"entry": "pro.html",
"styles": [
"theme.css",
"blocks.css"
],
"page_templates": [
"home",
"about",
"contact",
"listing",
"category",
"product",
"article",
"brand",
"case",
"service",
"faq",
"standard",
"notfound"
],
"blocks": [
"hero",
"richtext",
"features",
"stats",
"collection",
"image_text",
"gallery",
"faq",
"cta",
"attributes",
"spacer"
],
"responsive": true,
"auto_binding": true,
"professional_layout": true,
"commercial_ui": true,
"commercial_features": [
"responsive-design",
"product-detail",
"brand-collections",
"gallery",
"content-facets",
"mega-navigation",
"contact-integration",
"readiness-audit"
]
}
@@ -0,0 +1,285 @@
{
"home": [
{
"id": "0cdf18bf7f",
"type": "hero",
"visible": true,
"title": "工程质量,值得信赖",
"subtitle": "INDUSTRIAL ENGINEERING",
"description": "{{site.tagline}}",
"button": "查看产品",
"href": "/product/",
"align": "left"
},
{
"id": "f846a2ff6b",
"type": "features",
"visible": true,
"title": "制造与服务能力",
"items": [
{
"title": "专业服务",
"text": "从咨询到交付"
},
{
"title": "品质保障",
"text": "严格的质量流程"
},
{
"title": "全球网络",
"text": "连接不同市场"
}
]
},
{
"id": "ff4655d684",
"type": "collection",
"visible": true,
"title": "工业产品系列",
"subtitle": "PRODUCT SOLUTIONS",
"source": "product",
"limit": 6,
"columns": 3,
"button": "查看全部",
"href": "/product/"
},
{
"id": "4355daaba0",
"type": "image_text",
"visible": true,
"title": "工程标准与服务流程",
"subtitle": "QUALITY FIRST",
"description": "展示业务流程和服务理念。",
"image": "",
"image_side": "right",
"button": "了解更多",
"href": "/about/"
},
{
"id": "9e5056370d",
"type": "cta",
"visible": true,
"title": "获取专业产品方案",
"subtitle": "告诉我们您的需求,我们将提供适合的方案。",
"button": "联系我们",
"href": "/contact/"
}
],
"about": [
{
"id": "8ced1e0493",
"type": "image_text",
"visible": true,
"title": "认识我们",
"subtitle": "OUR APPROACH",
"description": "展示业务流程和服务理念。",
"image": "",
"image_side": "right",
"button": "了解更多",
"href": "/about/"
},
{
"id": "52b6064084",
"type": "stats",
"visible": true,
"title": "我们的实力",
"items": [
{
"value": "50,000+",
"label": "服务案例"
},
{
"value": "70+",
"label": "市场覆盖"
},
{
"value": "30+",
"label": "合作伙伴"
}
]
},
{
"id": "cf39bcb75d",
"type": "features",
"visible": true,
"title": "我们的优势",
"items": [
{
"title": "专业服务",
"text": "从咨询到交付"
},
{
"title": "品质保障",
"text": "严格的质量流程"
},
{
"title": "全球网络",
"text": "连接不同市场"
}
]
}
],
"contact": [
{
"id": "597f6ffb08",
"type": "richtext",
"visible": true,
"title": "联系我们",
"body": "<p>请填写企业联系方式与地址。</p>"
},
{
"id": "8c02c61415",
"type": "cta",
"visible": true,
"title": "欢迎联系",
"subtitle": "告诉我们您的需求,我们将提供适合的方案。",
"button": "联系我们",
"href": "/contact/"
}
],
"listing": [
{
"id": "2c3106baf6",
"type": "collection",
"visible": true,
"title": "工业产品目录",
"subtitle": "为客户提供适合的解决方案",
"source": "product",
"limit": 24,
"columns": 3,
"button": "查看全部",
"href": "/product/"
}
],
"product": [
{
"id": "42925dc8e3",
"type": "image_text",
"visible": true,
"title": "产品介绍",
"subtitle": "OUR APPROACH",
"description": "{{page.summary}}",
"image": "{{page.cover}}",
"image_side": "right",
"button": "了解更多",
"href": "/about/"
},
{
"id": "503266af35",
"type": "attributes",
"visible": true,
"title": "产品规格与参数"
},
{
"id": "b2b1f5293c",
"type": "richtext",
"visible": true,
"title": "产品详情",
"body": "{{page.body}}"
},
{
"id": "3837f66074",
"type": "cta",
"visible": true,
"title": "开启新的合作",
"subtitle": "告诉我们您的需求,我们将提供适合的方案。",
"button": "联系我们",
"href": "/contact/"
}
],
"article": [
{
"id": "c2573cb88d",
"type": "richtext",
"visible": true,
"title": "{{page.title}}",
"body": "{{page.body}}"
}
],
"brand": [
{
"id": "f43b705317",
"type": "image_text",
"visible": true,
"title": "{{page.title}}",
"subtitle": "OUR APPROACH",
"description": "{{page.summary}}",
"image": "{{page.cover}}",
"image_side": "right",
"button": "了解更多",
"href": "/about/"
},
{
"id": "b3faf08170",
"type": "collection",
"visible": true,
"title": "品牌产品",
"subtitle": "为客户提供适合的解决方案",
"source": "product",
"limit": 6,
"columns": 3,
"button": "查看全部",
"href": "/product/"
}
],
"standard": [
{
"id": "7937550319",
"type": "richtext",
"visible": true,
"title": "{{page.title}}",
"body": "{{page.body}}"
}
],
"notfound": [
{
"id": "59383d762e",
"type": "cta",
"visible": true,
"title": "页面暂时无法访问",
"subtitle": "告诉我们您的需求,我们将提供适合的方案。",
"button": "返回首页",
"href": "/"
}
],
"case": [
{
"type": "image_text",
"title": "{{page.title}}",
"image": "{{page.cover}}",
"description": "{{page.summary}}"
},
{
"type": "richtext",
"title": "项目概述",
"body": "{{page.body}}"
}
],
"service": [
{
"type": "richtext",
"title": "{{page.title}}",
"body": "{{page.body}}"
},
{
"type": "cta",
"title": "了解更多",
"href": "/contact/"
}
],
"faq": [
{
"type": "faq",
"title": "常见问题",
"items": []
}
],
"category": [
{
"type": "collection",
"title": "分类产品",
"source": "product",
"limit": 24
}
]
}
@@ -0,0 +1,4 @@
<div class="pro-v30-page pro-v30-about pro-v30-about-industrial"><section class="pro-v30-about-opening"><div class="pro-wrap"><span class="pro-eyebrow">OUR COMPANY / APPROACH</span><div class="pro-v30-about-heading"><h1>{{page.title}}</h1><p>{{page.summary or site.tagline or '了解材料、产品与工程应用'}}</p></div></div></section>
<section class="pro-section pro-v30-about-story"><div class="pro-wrap pro-v30-about-grid"><div class="pro-v30-about-media">{% if page.cover %}<img src="{{safe_image(page.cover)}}" alt="{{page.title}}" loading="lazy">{% else %}<div class="pro-v30-media-placeholder" aria-hidden="true"><span>OUR / STORY</span></div>{% endif %}</div><div class="pro-v30-about-copy"><span class="pro-eyebrow">ABOUT / 企业与制造</span><h2>面向实际工程需求的产品与服务</h2>{{body_copy(content,page)}}<a class="pro-v30-text-action" href="/contact/">联系与合作 ↗</a></div></div></section>
{% if features %}<section class="pro-section pro-v30-about-services"><div class="pro-wrap">{{section_title('WHAT WE DO','相关服务','/services/')}}{{grid(features,'service')}}</div></section>{% endif %}
{% if cases %}<section class="pro-section pro-v30-about-cases"><div class="pro-wrap">{{section_title('SELECTED WORK','相关项目','/cases/')}}{{grid(cases,'case')}}</div></section>{% endif %}</div>
@@ -0,0 +1,2 @@
<div class="pro-v30-page pro-v30-article pro-v30-article-industrial"><section class="pro-article-hero pro-v30-reading-head"><div class="pro-wrap"><nav class="pro-v30-crumb" aria-label="面包屑导航"><a href="/">首页</a><span aria-hidden="true">/</span><a href="/news/">技术与行业动态</a></nav><div class="pro-v30-reading-title"><span class="pro-eyebrow">IDEAS / JOURNAL</span><h1>{{page.title}}</h1>{% if page.summary %}<p>{{page.summary}}</p>{% endif %}{% if fields.get('publish_date') %}<time datetime="{{fields.get('publish_date')}}">{{fields.get('publish_date')}}</time>{% endif %}</div></div></section>
<div class="pro-wrap pro-v30-reading-layout"><aside class="pro-v30-reading-aside"><span class="pro-eyebrow">READ / EXPLORE</span><a href="/news/">← 返回资讯</a><span class="pro-v30-reading-rule" aria-hidden="true"></span><a href="/contact/">联系沟通 ↗</a></aside><article id="article-body" class="pro-v30-reading-content">{% if page.cover %}<figure><img src="{{safe_image(page.cover)}}" alt="{{page.title}}" loading="eager"></figure>{% endif %}{{body_copy(content,page)}}<a class="pro-v30-subtle-link" href="/news/">← 返回资讯</a></article></div></div>
@@ -0,0 +1 @@
<section class="pro-inner-hero pro-brand-hero"><div class="pro-wrap pro-brand-hero-grid"><div><span class="pro-eyebrow">BRAND PROFILE / PARTNERS</span><h1>{{page.title}}</h1><p>{{page.summary}}</p></div><div class="pro-brand-media">{{cover(page,'brand')}}</div></div></section><section class="pro-section"><div class="pro-wrap">{{body_copy(content,page)}}{% if related %}{{section_title('PRODUCT LINE','相关产品','/products/')}}{{grid(related,'product')}}{% endif %}</div></section>
@@ -0,0 +1 @@
<section class="pro-inner-hero"><div class="pro-wrap"><span class="pro-eyebrow">SELECTED WORK / PROJECT</span><h1>{{page.title}}</h1><p>{{page.summary}}</p></div></section><section class="pro-section"><div class="pro-wrap pro-case-detail">{% if page.cover %}<img src="{{safe_image(page.cover)}}" alt="{{page.title}}">{% endif %}<div class="pro-case-meta">{% if fields.get('location') %}<span>项目地点<br><strong>{{fields.get('location')}}</strong></span>{% endif %}{% if fields.get('industry') %}<span>所属行业<br><strong>{{fields.get('industry')}}</strong></span>{% endif %}</div>{{body_copy(content,page)}}<a class="pro-text-link" href="/cases/">浏览更多案例 →</a></div></section>
@@ -0,0 +1,2 @@
<section class="pro-inner-hero pro-listing-hero"><div class="pro-wrap"><span class="pro-eyebrow">DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}</span><h1>{{page.title}}</h1><p>{{page.summary or '查看此栏目的全部已发布内容。'}}</p><div class="pro-breadcrumb"><a href="/">首页</a><span>/</span><strong>{{page.title}}</strong></div></div></section>
<section class="pro-section pro-list-section"><div class="pro-wrap">{{ filters() }}<div class="pro-filter-scope">{{grid(listing,'catalog')}}</div>{% if pagination and pagination.pages>1 %}<nav class="pro-pages-nav" aria-label="商品列表分页">{% if pagination.previous %}<a href="{{pagination.previous}}" rel="prev">← 上一页</a>{% endif %}<span>第 {{pagination.page}} / {{pagination.pages}} 页</span>{% if pagination.next %}<a href="{{pagination.next}}" rel="next">下一页 →</a>{% endif %}</nav>{% endif %}</div></section>
@@ -0,0 +1,6 @@
<div class="pro-v30-page pro-v30-contact pro-v30-contact-industrial"><section class="pro-v30-contact-header"><div class="pro-wrap"><span class="pro-eyebrow">CONNECT / CONTACT</span><h1>{{page.title}}</h1><p>{{page.summary or '提交产品、规格和应用场景需求,方便进一步沟通。'}}</p></div></section>
<section class="pro-section pro-v30-contact-content"><div class="pro-wrap pro-v30-contact-layout"><div class="pro-v30-contact-info"><span class="pro-eyebrow">LET'S START A CONVERSATION</span><h2>咨询工程材料</h2>{{body_copy(content,page)}}
{% if footer_email %}<div class="pro-v30-contact-channel"><span>电子邮箱</span><a href="mailto:{{footer_email}}">{{footer_email}} ↗</a></div>{% endif %}
{% if not footer_email and not form_endpoint %}<p class="pro-muted">联系渠道正在更新,请稍后访问本页面。</p>{% endif %}
</div><div class="pro-v30-contact-form-wrap">{% if form_endpoint %}<span class="pro-eyebrow">SEND AN INQUIRY</span><h3>提交咨询</h3><form class="pro-inquiry" action="{{form_endpoint}}" method="POST"><label>姓名 <input name="name" autocomplete="name" required maxlength="100"></label><label>邮箱 <input name="email" type="email" autocomplete="email" required maxlength="180"></label><label>咨询内容 <textarea name="message" required minlength="6" maxlength="5000"></textarea></label><input name="website" tabindex="-1" autocomplete="off" class="pro-honeypot" aria-hidden="true"><button type="submit" class="pro-button">发送咨询 ↗</button><p>提交信息将发送至配置的外部表单服务,请了解其隐私政策。</p></form>
{% else %}<div class="pro-v30-contact-note"><span class="pro-eyebrow">HOW TO REACH US</span><p>您可以通过本页提供的联系渠道咨询产品、项目或合作事项。</p><span class="pro-v30-decorative-number" aria-hidden="true">↗</span></div>{% endif %}</div></div></section></div>
@@ -0,0 +1,2 @@
<section class="pro-inner-hero pro-info-hero"><div class="pro-wrap"><span class="pro-eyebrow">{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}</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">{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}</span><h2>{{'让我们开始沟通。' if kind=='contact' else page.title}}</h2>{% if kind=='contact' and footer_email %}<a class="pro-contact-email" href="mailto:{{footer_email}}">{{footer_email}} ↗</a>{% elif kind=='contact' %}<p class="pro-muted">请先在网站管理后台填写正式联系邮箱。</p>{% endif %}</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>
@@ -0,0 +1,6 @@
<section class="pro-home-hero pro-industrial-hero"><div class="pro-wrap pro-home-grid"><div class="pro-hero-copy"><span class="pro-eyebrow">ENGINEERED FOR WHAT'S NEXT / 001</span><h1>{{page.title if page.title not in (site.name,'首页','Home') else site.name}}<em> BUILT TO PERFORM.</em></h1><p>{{page.summary or site.tagline or '从材料到解决方案,一站式展示企业实力。'}}</p><div class="pro-hero-actions"><a class="pro-button" href="/products/">查看产品系列 ↗</a><a class="pro-outline-button" href="/contact/">获取方案</a></div></div><div class="pro-hero-art">{% if page.cover %}<img src="{{safe_image(page.cover)}}" alt="{{page.title}}">{% else %}<div class="pro-technical"><div></div><div></div><div></div><strong>FORM / FUNCTION</strong></div>{% endif %}</div></div></section>
{% if categories %}<section class="pro-section pro-category-band"><div class="pro-wrap">{{section_title('PRODUCT SYSTEMS','从类别开始探索','/products/')}}<div class="pro-category-tiles">{% for cat in categories[:6] %}<a href="{{cat.url}}"><span>0{{loop.index}}</span><strong>{{cat.name}}</strong><b>↗</b></a>{% endfor %}</div></div></section>{% endif %}
<section class="pro-section pro-featured"><div class="pro-wrap">{{section_title('PRODUCT PORTFOLIO','核心产品','/products/')}}{{grid(showcase,'industrial')}}</div></section>
<section class="pro-section pro-statement"><div class="pro-wrap pro-statement-layout"><span class="pro-eyebrow">APPLICATIONS & SUPPORT</span><div><h2>专注可靠交付,服务多元项目。</h2><p>{{site.tagline or '企业技术、材料和服务能力展示。'}}</p><a class="pro-text-link" href="/services/">服务与解决方案 ↗</a></div></div></section>
{% if cases %}<section class="pro-section pro-v30-home-editorial"><div class="pro-wrap">{{section_title('APPLICATIONS / CASES','应用项目','/cases/')}}{{grid(cases,'case')}}</div></section>{% endif %}
@@ -0,0 +1,6 @@
<div class="pro-v30-page pro-v30-listing pro-v30-technical-index">
<section class="pro-v30-page-intro"><div class="pro-wrap pro-v30-intro-layout"><div class="pro-v30-intro-main"><span class="pro-eyebrow">ENGINEERING MATERIALS / INDEX</span><h1>{{page.title}}</h1><p>{{page.summary or '通过材料分类与规格快速查找产品'}}</p></div><div class="pro-v30-intro-aside"><span class="pro-v30-overline">BROWSE / EXPLORE</span><span class="pro-v30-index-number">{{'%02d'|format(listing|length)}}</span><span class="pro-v30-caption">本页展示内容</span></div></div></section>
<section class="pro-section pro-v30-index-body"><div class="pro-wrap"><div class="pro-v30-index-toolbar"><div class="pro-v30-section-title"><span class="pro-eyebrow">CONTENTS / FIND YOUR FIT</span><h2>产品与材料</h2></div><a class="pro-v30-subtle-link" href="/contact/">需要协助?联系沟通 ↗</a></div>
{{filters()}}<div class="pro-filter-scope pro-v30-results" aria-live="polite">{{grid(listing,'catalog')}}</div>
{% if pagination and pagination.pages>1 %}<nav class="pro-pages-nav" aria-label="列表分页">{% if pagination.previous %}<a href="{{pagination.previous}}" rel="prev">← 上一页</a>{% endif %}<span>第 {{pagination.page}} / {{pagination.pages}} 页</span>{% if pagination.next %}<a href="{{pagination.next}}" rel="next">下一页 →</a>{% endif %}</nav>{% endif %}
</div></section></div>
@@ -0,0 +1 @@
<section class="pro-404 pro-wrap"><span>ERROR / 404</span><h1>404</h1><p>抱歉,你访问的页面不存在,或者已被移动。</p><a class="pro-button" href="/">返回首页 ↗</a></section>
@@ -0,0 +1,4 @@
<div class="pro-product-layout pro-v30-page pro-v30-product pro-v30-product-industrial"><section class="pro-v30-product-top"><div class="pro-wrap"><nav class="pro-v30-crumb" aria-label="面包屑导航"><a href="/">首页</a><span aria-hidden="true">/</span><a href="/products/">产品与材料</a><span aria-hidden="true">/</span><span aria-current="page">{{page.title}}</span></nav>
<div class="pro-v30-product-head"><div class="pro-v30-product-intro"><span class="pro-eyebrow">ENGINEERING MATERIALS / DETAILS</span><h1>{{page.title}}</h1><p>{{page.summary or ''}}</p><div class="pro-v30-tags">{% if fields.get('brand') %}<span>{{fields.get('brand')}}</span>{% endif %}{% if fields.get('category') %}<span>{{fields.get('category')}}</span>{% endif %}</div><a class="pro-button pro-v30-action" href="/contact/">咨询相关信息 ↗</a></div>
<div class="pro-v30-product-visual"><div class="pro-product-media pro-v30-main-media">{{cover(page,'product')}}</div>{% if gallery|length > 1 %}<div class="pro-gallery-thumbs" aria-label="产品展示">{% for photo in gallery %}<button type="button" data-gallery-image="{{photo}}" aria-label="查看第{{loop.index}}张图片"><img src="{{photo}}" alt="{{page.title}} · 图片{{loop.index}}" loading="lazy"></button>{% endfor %}</div>{% endif %}</div></div></div></section>
<section class="pro-section pro-v30-product-information"><div class="pro-wrap pro-v30-product-details"><div class="pro-v30-details-main"><span class="pro-eyebrow">OVERVIEW / 产品资料</span><h2>产品资料</h2>{{body_copy(content,page)}}</div><aside class="pro-v30-details-side"><span class="pro-eyebrow">TECHNICAL / FACTS</span><h2>技术规格</h2><div class="pro-specs">{% for row in specs %}<div><span>{{row.label}}</span><strong>{{row.value}}</strong></div>{% else %}<p class="pro-muted">更多资料请通过联系页面咨询。</p>{% endfor %}</div></aside></div></section></div>
@@ -0,0 +1,2 @@
<section class="pro-inner-hero"><div class="pro-wrap"><span class="pro-eyebrow">SEARCH / CONTENT</span><h1>站内搜索</h1><p>搜索本网站的产品、品牌、新闻和案例。</p></div></section>
<main id="main-content" class="sf-search-page pro-wrap"><label for="sf-search">输入关键词</label><input type="search" id="sf-search" placeholder="搜索产品、品牌、新闻、案例……" autofocus><div id="sf-search-count" aria-live="polite"></div><div id="sf-search-results"></div></main>
@@ -0,0 +1,2 @@
<section class="pro-inner-hero pro-info-hero"><div class="pro-wrap"><span class="pro-eyebrow">{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}</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">{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}</span><h2>{{'让我们开始沟通。' if kind=='contact' else page.title}}</h2>{% if kind=='contact' and footer_email %}<a class="pro-contact-email" href="mailto:{{footer_email}}">{{footer_email}} ↗</a>{% elif kind=='contact' %}<p class="pro-muted">请先在网站管理后台填写正式联系邮箱。</p>{% endif %}</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>
@@ -0,0 +1,2 @@
<section class="pro-inner-hero pro-info-hero"><div class="pro-wrap"><span class="pro-eyebrow">{{ 'GET IN TOUCH' if kind=='contact' else ('OUR STORY' if kind=='about' else 'EXPLORE') }}</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">{{'CONNECT / CONTACT' if kind=='contact' else 'ABOUT / DISCOVER'}}</span><h2>{{'让我们开始沟通。' if kind=='contact' else page.title}}</h2>{% if kind=='contact' and footer_email %}<a class="pro-contact-email" href="mailto:{{footer_email}}">{{footer_email}} ↗</a>{% elif kind=='contact' %}<p class="pro-muted">请先在网站管理后台填写正式联系邮箱。</p>{% endif %}</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>
+89
View File
@@ -0,0 +1,89 @@
:root{--pro-text:#173a57;--pro-muted:#6b8191;--pro-border:#d8e3eb;--pro-background:#fff;--pro-surface:#edf3f7;--pro-accent:#16629a;--pro-hero:#0e3959;--pro-radius:2px;--pro-headings:Arial,sans-serif;--pro-wide:1290px}
.pro-header{background:#fff}.pro-logo-symbol{color:#14649b;font-size:38px}.pro-utility{background:#0d3a5a;color:#c6e2f1}.pro-header-cta{border-radius:2px;background:#0c5b94}.pro-industrial-hero{background:linear-gradient(90deg,#eff4f8 0%,#e6eef5 60%,#d7e5f0 100%);color:#123b59}.pro-industrial-hero .pro-hero-copy{padding-right:6%}.pro-industrial-hero .pro-hero-copy h1{font-size:clamp(45px,5.3vw,80px)}.pro-industrial-hero .pro-hero-copy h1 em{color:#186fa4}.pro-industrial-hero .pro-hero-art{border-radius:0;background:#0c466d}.pro-technical{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:3%;overflow:hidden;background:repeating-linear-gradient(0deg,transparent,transparent 39px,#ffffff0b 40px),repeating-linear-gradient(90deg,transparent,transparent 39px,#ffffff0b 40px)}.pro-technical>div{width:21%;height:55%;border-radius:60px;border:27px solid #6ba6d4;transform:skewY(-13deg) rotate(-5deg);box-shadow:18px 17px 0 #14324a,0 0 0 9px #bde2fb inset,12px 22px 50px #081c30}.pro-technical>div:nth-child(2){height:72%;width:27%;border-width:34px}.pro-technical strong{position:absolute;bottom:24px;left:28px;color:#aed1e8;font-size:13px;letter-spacing:.17em}.pro-outline-button{color:#0f5988}.pro-category-band{background:#fff}.pro-category-tiles a{background:#eaf2f7;border-top:4px solid #2176aa;min-height:185px;transition:background .2s}.pro-category-tiles a:hover{background:#dce9f2}.pro-category-tiles strong{font-size:21px}.pro-featured{background:#edf3f7}.pro-card{background:#fff;border-radius:2px}.pro-card-cover{background:#d8e6ef}.pro-art-industrial,.pro-art-product{background:linear-gradient(135deg,#aecadf,#e1f1fb)}.pro-art-center{color:#45789b99}.pro-statement{background:#103e5e;color:#fff}.pro-statement .pro-eyebrow{color:#8dc8eb}.pro-statement p{color:#d3e1e9}.pro-statement .pro-text-link{color:#9cdaff}.pro-inner-hero{background:linear-gradient(115deg,#edf4f9 0%,#d9e9f4 100%)}.pro-inner-hero:after{content:'+';position:absolute;right:11%;top:6%;font-size:350px;font-weight:100;color:#15699b10;line-height:1}.pro-article-hero{background:#dceaf4}.pro-article-hero p{color:#537088}.pro-footer{background:#092c45}.pro-end-cta{background:#0a4772}.pro-end-cta a{background:#fff;color:#144b75!important}.pro-404 h1{color:#156d9d}
/* SiteForge V5 commercial signature */
.pro-industrial-hero .pro-hero-art{background-image:url("./visual/hero.svg")!important;background-position:center;background-size:cover;}
.pro-industrial-hero .pro-hero-art{border-radius:0;box-shadow:25px 25px 0 #ffffff9c}.pro-industrial-hero .pro-hero-art .pro-technical{opacity:0}.pro-category-tiles a{transition:transform .2s,background .2s}.pro-category-tiles a:hover{transform:translateY(-4px);background:#ddeef9}.pro-card{border-radius:0;border:1px solid #cddfe9}.pro-card-cover{background:linear-gradient(145deg,#dceaf0,#c3d9e7)}.pro-art{background:repeating-linear-gradient(0deg,#e2edf4,#e2edf4 24px,#d6e7ef 25px);color:#20577e}.pro-featured{background:#f4f8fb}.pro-statement{background:#103b57;color:#fff}.pro-statement p{color:#d4e5ed}.pro-statement .pro-text-link{color:#a8d8ee}.pro-footer{background:#0e3046;color:#eff5f9}
/* V8.29 presentation refinement: industrial. Preserve per-theme design language. */
body.pro-industrial{--pro-studio-ring:rgba(24,101,145,.19);--pro-focus-color:#13709b;--pro-interact-radius:7px}
body.pro-industrial .pro-wrap{max-width:100%;min-width:0}
body.pro-industrial .pro-hero-copy,body.pro-industrial .pro-card-body,body.pro-industrial .pro-card-copy{min-width:0;overflow-wrap:anywhere}
body.pro-industrial .pro-hero-copy h1{font-size:clamp(2.45rem,5.15vw,5.6rem);line-height:1.08;max-width:18ch;text-wrap:balance}
body.pro-industrial .pro-section-head h2{font-size:clamp(1.85rem,3vw,3.25rem);text-wrap:balance}
body.pro-industrial .pro-card,body.pro-industrial .pro-product-card{transition:transform .18s ease,box-shadow .18s ease,border-color .18s ease}
@media(hover:hover){body.pro-industrial .pro-card:hover,body.pro-industrial .pro-product-card:hover{transform:translateY(-3px);box-shadow:0 14px 36px var(--pro-studio-ring)}}
body.pro-industrial :where(button,a,input,select,textarea):focus-visible{outline:3px solid var(--pro-focus-color);outline-offset:3px}
body.pro-industrial img{max-width:100%;height:auto}
body.pro-industrial .pro-card-cover img,body.pro-industrial .pro-gallery-main img{object-fit:cover;max-height:100%}
body.pro-industrial .pro-mobile-toggle,body.pro-industrial .pro-nav-disclose{min-height:44px;min-width:44px}
@media(max-width:860px){body.pro-industrial .pro-home-grid,body.pro-industrial .pro-product-layout,body.pro-industrial .pro-inquiry-wrap{min-width:0}body.pro-industrial .pro-card-grid,body.pro-industrial .pro-grid{gap:clamp(12px,3vw,23px)}}
@media(max-width:560px){body.pro-industrial .pro-wrap{padding-left:max(16px,env(safe-area-inset-left));padding-right:max(16px,env(safe-area-inset-right))}body.pro-industrial .pro-hero-copy h1{font-size:clamp(2rem,9vw,3.2rem)}body.pro-industrial .pro-section-head h2{font-size:clamp(1.75rem,7vw,2.5rem)}body.pro-industrial .pro-section{padding-block:clamp(34px,9vw,60px)}body.pro-industrial .pro-card-cover{height:auto;aspect-ratio:4/3}body.pro-industrial .pro-card-cover img{width:100%;height:100%;object-fit:cover}}
@media(prefers-reduced-motion:reduce){body.pro-industrial .pro-card,body.pro-industrial .pro-product-card{transition:none!important;transform:none!important}}
/* V8.30 REAL PAGE COMPOSITION. Built-in theme version 1.3.0 only.
Content-driven sections do not invent client names, certifications or KPIs. */
.pro-v30-page{--v30-ink:var(--pro-text);--v30-secondary:var(--pro-muted);--v30-line:var(--pro-border);--v30-panel:var(--pro-surface);--v30-max:1180px;color:var(--v30-ink);overflow:clip}
.pro-v30-page .pro-wrap{max-width:var(--v30-max);min-width:0}
.pro-v30-page h1,.pro-v30-page h2{overflow-wrap:anywhere;text-wrap:balance;line-height:1.11}
.pro-v30-page h1{font-size:clamp(2.55rem,5.2vw,5.6rem);margin:18px 0 22px;letter-spacing:-.035em}
.pro-v30-page h2{font-size:clamp(1.8rem,3.3vw,3rem);margin:16px 0 26px;letter-spacing:-.02em}
.pro-v30-page p{line-height:1.75;overflow-wrap:anywhere}
.pro-v30-page img{max-width:100%}
.pro-v30-intro-layout{display:grid;grid-template-columns:minmax(0,1fr) 190px;gap:8vw;align-items:end}
.pro-v30-page-intro{padding:clamp(68px,10vw,128px) 0 clamp(55px,8vw,94px);background:var(--v30-panel)}
.pro-v30-intro-main>p{max-width:610px;font-size:clamp(16px,2vw,21px);color:var(--v30-secondary)}
.pro-v30-intro-aside{border-left:1px solid var(--v30-line);padding-left:27px;display:grid;gap:8px;align-content:end;min-height:120px}
.pro-v30-overline,.pro-v30-caption{font-size:11px;letter-spacing:.12em;color:var(--v30-secondary)}
.pro-v30-index-number{font-size:64px;line-height:1;font-weight:650;letter-spacing:-.08em}
.pro-v30-index-toolbar{display:flex;justify-content:space-between;align-items:end;gap:22px;margin-bottom:28px}
.pro-v30-index-body{background:var(--pro-background)}
.pro-v30-index-body .pro-section-head,.pro-v30-index-toolbar h2{margin:8px 0}
.pro-v30-subtle-link,.pro-v30-text-action{display:inline-flex;gap:10px;text-decoration:none;font-weight:750;font-size:13px;align-items:center;min-height:44px}
.pro-v30-subtle-link:hover,.pro-v30-text-action:hover{text-decoration:underline}
.pro-v30-results{margin-top:clamp(28px,5vw,54px)}
.pro-v30-product-top{padding:26px 0 clamp(60px,8vw,110px);background:var(--v30-panel)}
.pro-v30-crumb{display:flex;flex-wrap:wrap;gap:11px;align-items:center;font-size:12px;color:var(--v30-secondary);margin:8px 0 46px}
.pro-v30-crumb a{text-decoration:underline;text-underline-offset:4px}
.pro-v30-product-head{display:grid;grid-template-columns:minmax(290px,.78fr) minmax(0,1.22fr);align-items:center;gap:clamp(30px,6vw,90px)}
.pro-v30-product-intro>p{font-size:clamp(16px,2vw,19px);color:var(--v30-secondary);max-width:48ch}
.pro-v30-tags{display:flex;gap:8px;flex-wrap:wrap;margin:24px 0 30px}.pro-v30-tags span{padding:8px 13px;border:1px solid var(--v30-line);font-size:12px;border-radius:2px}
.pro-v30-action{display:inline-flex;align-items:center;justify-content:center;min-height:48px}
.pro-v30-main-media{background:var(--pro-surface);aspect-ratio:4/3;overflow:hidden;display:grid;place-items:center;border-radius:var(--pro-radius)}
.pro-v30-main-media .pro-photo{width:100%;height:100%;object-fit:contain}
.pro-v30-product-details{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(270px,.7fr);gap:clamp(30px,7vw,104px)}
.pro-v30-details-main .pro-rich{max-width:760px}.pro-v30-details-side{align-self:start;border-top:3px solid var(--pro-accent);padding-top:25px}
.pro-v30-details-side .pro-specs>div{grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);gap:16px}.pro-v30-details-side .pro-specs strong{overflow-wrap:anywhere}
.pro-v30-reading-head{padding:clamp(50px,8vw,100px) 0 clamp(60px,8vw,100px);background:var(--v30-panel)}
.pro-v30-reading-title{max-width:940px;margin:auto}.pro-v30-reading-title>p{font-size:clamp(16px,2.1vw,22px);max-width:65ch;color:var(--v30-secondary)}
.pro-v30-reading-title time{display:block;margin-top:24px;font-size:13px;color:var(--v30-secondary)}
.pro-v30-reading-layout{display:grid;grid-template-columns:180px minmax(0,750px);gap:clamp(35px,7vw,110px);padding-block:clamp(55px,8vw,110px)}
.pro-v30-reading-aside{display:flex;flex-direction:column;gap:18px;align-self:start;position:sticky;top:30px;font-size:13px}
.pro-v30-reading-rule{height:1px;background:var(--v30-line)}
.pro-v30-reading-content{min-width:0}.pro-v30-reading-content figure{margin:0 0 45px}.pro-v30-reading-content figure img{display:block;width:100%;height:auto;max-height:550px;object-fit:cover}
.pro-v30-reading-content .pro-rich{line-height:1.9;font-size:clamp(16px,1.75vw,18px)}.pro-v30-reading-content .pro-rich>*+*{margin-top:1.3em}
.pro-v30-about-opening{padding:clamp(75px,10vw,130px) 0;background:var(--v30-panel)}
.pro-v30-about-heading{display:grid;grid-template-columns:1.3fr .7fr;gap:9%;align-items:end}.pro-v30-about-heading>p{font-size:clamp(17px,2vw,23px);color:var(--v30-secondary);padding-bottom:12px}
.pro-v30-about-grid{display:grid;grid-template-columns:1fr 1fr;gap:clamp(36px,8vw,120px);align-items:center}
.pro-v30-about-media{min-height:360px;aspect-ratio:5/4;background:var(--v30-panel);overflow:hidden}.pro-v30-about-media img{width:100%;height:100%;object-fit:cover}
.pro-v30-media-placeholder{height:100%;min-height:360px;display:grid;place-items:center;color:var(--v30-secondary);letter-spacing:.18em;font-size:12px;border:1px solid var(--v30-line)}
.pro-v30-about-copy .pro-rich{max-width:620px}.pro-v30-about-cases{background:var(--v30-panel)}
.pro-v30-contact-header{background:var(--v30-panel);padding:clamp(75px,10vw,135px) 0}.pro-v30-contact-header p{font-size:clamp(17px,2vw,21px);max-width:58ch;color:var(--v30-secondary)}
.pro-v30-contact-layout{display:grid;grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);gap:clamp(40px,8vw,120px)}
.pro-v30-contact-channel{display:grid;gap:10px;margin-top:45px;border-top:1px solid var(--v30-line);padding-top:25px}.pro-v30-contact-channel span{font-size:12px;color:var(--v30-secondary)}.pro-v30-contact-channel a{font-size:clamp(15px,2vw,24px);overflow-wrap:anywhere;font-weight:650}
.pro-v30-contact-form-wrap{padding:clamp(22px,4vw,48px);background:var(--v30-panel);min-width:0}
.pro-v30-contact-form-wrap .pro-inquiry{margin-top:22px}.pro-v30-contact-form-wrap .pro-inquiry input,.pro-v30-contact-form-wrap textarea{background:var(--pro-background)}
.pro-v30-contact-note{display:grid;align-content:space-between;min-height:270px;gap:24px}.pro-v30-decorative-number{font-size:90px;line-height:1;justify-self:end;color:var(--pro-accent)}
.pro-v30-home-editorial{background:var(--pro-surface)}
.pro-v30-page :where(a,button,input,textarea,select):focus-visible{outline:3px solid var(--pro-accent);outline-offset:3px}
@media(max-width:950px){.pro-v30-product-head,.pro-v30-product-details,.pro-v30-about-grid,.pro-v30-contact-layout{grid-template-columns:1fr;gap:38px}.pro-v30-product-visual{order:-1}.pro-v30-about-heading{grid-template-columns:1fr;gap:4px}.pro-v30-about-media{max-height:430px}.pro-v30-reading-layout{grid-template-columns:1fr;gap:24px}.pro-v30-reading-aside{position:static;flex-direction:row;align-items:center;flex-wrap:wrap}.pro-v30-reading-rule{display:none}}
@media(max-width:600px){.pro-v30-page .pro-wrap{padding-inline:max(18px,env(safe-area-inset-left));}.pro-v30-page h1{font-size:clamp(2.2rem,9.2vw,3.4rem)}.pro-v30-intro-layout{grid-template-columns:1fr;gap:18px}.pro-v30-intro-aside{display:none}.pro-v30-index-toolbar{align-items:start;flex-direction:column;gap:0}.pro-v30-crumb{margin-bottom:24px}.pro-v30-product-top{padding-bottom:42px}.pro-v30-main-media{aspect-ratio:5/4}.pro-v30-product-details{gap:45px}.pro-v30-reading-title{margin:0}.pro-v30-reading-content figure{margin-bottom:22px}.pro-v30-about-media,.pro-v30-media-placeholder{min-height:220px}.pro-v30-contact-form-wrap{padding:20px}.pro-v30-contact-layout{gap:30px}}
@media(prefers-reduced-motion:reduce){.pro-v30-page *{scroll-behavior:auto!important}}
/* Theme-specific editorial composition */
.pro-v30-technical-index .pro-v30-page-intro{background:#eaf2f4;border-bottom:5px solid #154c62}.pro-v30-product-industrial .pro-v30-product-top{background:#eef3f4}.pro-v30-product-industrial .pro-v30-main-media{background:#fff;border:1px solid #d4e0e4}.pro-v30-product-industrial .pro-v30-main-media .pro-photo{object-fit:contain;padding:clamp(8px,2vw,24px)}.pro-v30-product-industrial .pro-v30-details-side{background:#edf3f5;padding:27px;border:1px solid #d8e4e7;border-top:4px solid #24718e}.pro-v30-article-industrial .pro-v30-reading-head{border-left:solid clamp(6px,1vw,18px) #276780}.pro-v30-about-industrial .pro-v30-about-media{border-radius:4px}.pro-v30-contact-industrial .pro-v30-contact-form-wrap{background:#eef4f6}
/* Retain existing gallery runtime behavior with re-composed product layout. */
.pro-v30-page .pro-product-media{height:auto;min-height:0}
+13
View File
@@ -0,0 +1,13 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><base href="/"><title>{{ seo_title }}</title><meta name="description" content="{{ seo_description }}"><meta property="og:type" content="{{ 'article' if kind in ('article','case') else 'website' }}"><meta property="og:title" content="{{ seo_title }}"><meta property="og:description" content="{{ seo_description }}">{% if canonical %}<meta property="og:url" content="{{canonical}}">{% endif %}<meta name="theme-color" content="{{theme_color}}"><meta name="robots" content="{{ 'noindex,follow' if noindex else page.get('robots','index,follow') }}">{% if canonical and not noindex %}<link rel="canonical" href="{{ canonical }}">{% endif %}<link rel="stylesheet" href="assets/theme.css"><link rel="stylesheet" href="assets/blocks.css"><link rel="stylesheet" href="assets/factory.css"><link rel="stylesheet" href="assets/pro-base.css"><link rel="stylesheet" href="assets/pro.css"><link rel="stylesheet" href="assets/commercial.css">{% if theme_overrides %}<link rel="stylesheet" href="assets/theme-overrides.css">{% endif %}<link rel="stylesheet" href="assets/custom.css"></head><body class="sf-theme-industrial pro-industrial"><a class="pro-skip" href="#main-content">跳转至正文</a>
{% from '_professional_components.html' import card,cover,grid,section_title,filters,footer_links,body_copy with context %}
{% if chrome_is_custom %}<div class="pro-utility"{% if not chrome.header.show_announcement %} hidden{% endif %}><div class="pro-wrap"><span>{{ chrome.header.announcement }}</span><span>{{ site.tagline or site.name }}</span></div></div>{% else %}<div class="pro-utility"><div class="pro-wrap"><span>ENGINEERING · MATERIALS · SUPPLY</span><span>INDUSTRIAL SOLUTIONS</span></div></div>{% endif %}
<header class="sf-site-head pro-header"><div class="pro-wrap pro-head-inner"><a class="pro-logo" href="/" aria-label="{{site.name}} 首页">{% if safe_image(site.logo) %}<img class="pro-brand-logo" src="{{safe_image(site.logo)}}" alt="{{site.name}} Logo">{% else %}<span class="pro-logo-symbol">▥</span>{% endif %}<span class="pro-logo-name">{{site.name}}<small> INDUSTRIAL</small></span></a><button class="pro-mobile-toggle" type="button" data-pro-menu aria-controls="pro-navigation" aria-expanded="false" aria-label="打开导航">☰</button><nav id="pro-navigation" class="pro-navigation" aria-label="主导航">{% for link in nav %}{% if link.children %}<div class="pro-nav-group"><a href="{{link.route}}">{{link.title}}</a><button type="button" class="pro-nav-disclose" aria-expanded="false" aria-label="展开{{link.title}}子菜单">▾</button><div class="pro-nav-children" hidden>{% for child in link.children %}<a href="{{child.route}}">{{child.title}}</a>{% for leaf in child.children %}<a class="pro-nav-leaf" href="{{leaf.route}}">— {{leaf.title}}</a>{% endfor %}{% endfor %}</div></div>{% else %}<a href="{{link.route}}">{{link.title}}</a>{% endif %}{% endfor %}<a href="/search/">搜索 ⌕</a>{% if chrome.header.show_search %}<a class="pro-search-link" href="/search/" aria-label="站内搜索">⌕</a>{% endif %}</nav><button class="pro-catalog-toggle" type="button" data-pro-catalog aria-controls="pro-mega-content" aria-expanded="false">产品分类 ▾</button>{% if chrome_is_custom %}{% if chrome.header.show_cta %}<a href="{{chrome.header.cta_href}}" class="pro-header-cta">{{chrome.header.cta_text}} <span>↗</span></a>{% endif %}{% else %}<a href="/contact/" class="pro-header-cta">询价咨询 <span>↗</span></a>{% endif %}</div>{% if categories %}<div class="pro-mega pro-wrap" data-pro-mega id="pro-mega-content" hidden><span>产品分类导航</span><div>{% for cat in categories[:12] %}<a href="{{cat.url}}">{{cat.name}} ↗</a>{% endfor %}</div></div>{% endif %}</header>
{% if not designer_replace %}{% include 'industrial/pages/' ~ kind ~ '.html' %}{% endif %}
{% if has_custom and kind!='notfound' %}<main id="main-content" class="sf-canvas pro-custom-content">{{custom_blocks}}</main>{% else %}<main id="main-content" class="sf-canvas pro-custom-content" aria-hidden="true"></main>{% endif %}
{% if kind not in ('notfound','search') %}<section class="pro-end-cta"><div class="pro-wrap"><div><span class="pro-eyebrow">LET'S CONNECT</span><h2>从这里开始下一步。</h2><p>{{site.tagline or '了解更多产品、服务与合作信息。'}}</p></div><a href="/contact/">联系我们 <span>↗</span></a></div></section>{% endif %}
<footer class="sf-site-foot pro-footer"><div class="pro-wrap"><div class="pro-footer-top"><div><span class="pro-eyebrow">INDUSTRIAL PARTNERSHIP</span><h2>{{chrome.footer.title or site.name}}</h2><p>{{footer_text}}</p></div><div class="pro-footer-links">{{footer_links(footer_nav)}}</div><div><span class="pro-eyebrow">BUSINESS ENQUIRY</span><p>{{footer_email or '请在后台配置企业联系方式'}}</p></div></div><div class="pro-footer-bottom"><span>© {{year}} {{site.name}}</span><span>ENGINEERED TO DELIVER</span></div></div></footer>
{% if chrome.mobile_tabs['enabled'] and chrome.mobile_tabs['items'] %}<nav class="sf-mobile-tabs" style="--sf-mobile-tab-count:{{ chrome.mobile_tabs['items']|length }}" aria-label="手机底部快捷导航" data-mobile-tabs>
{% for tab in chrome.mobile_tabs['items'] %}{% if tab.action == 'menu' %}<button type="button" class="sf-mobile-tab" data-mobile-open-menu aria-label="{{tab.title}}"><span class="sf-tab-icon" aria-hidden="true">{{ mobile_tab_icons[tab.icon] }}</span><span class="sf-tab-label">{{tab.title}}</span></button>{% else %}<a class="sf-mobile-tab" href="{{tab.route}}"{% if tab.route == route %} aria-current="page"{% endif %}><span class="sf-tab-icon" aria-hidden="true">{{ mobile_tab_icons[tab.icon] }}</span><span class="sf-tab-label">{{tab.title}}</span></a>{% endif %}{% endfor %}</nav>{% endif %}
<script defer src="assets/site-runtime.js"></script><script defer src="assets/pro.js"></script></body></html>
@@ -0,0 +1,12 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><base href="/"><title>{{seo_title}}</title><meta name="description" content="{{seo_description}}"><meta name="robots" content="{{'noindex,follow' if mode=='notfound' else (page.robots or 'index,follow')}}">{% if canonical %}<link rel="canonical" href="{{canonical}}">{% endif %}<link rel="stylesheet" href="assets/theme.css"><link rel="stylesheet" href="assets/blocks.css"><link rel="stylesheet" href="assets/custom.css"></head><body>
<div class="top"><div class="wrap"><span>ENGINEERING · MANUFACTURING · SUPPLY</span><span>GLOBAL INDUSTRIAL SOLUTIONS</span></div></div>
<header><div class="wrap header-inner"><a class="brand" href="./"><span class="brand-icon">▰</span><span>{{site.name}}<small>INDUSTRIAL SOLUTIONS</small></span></a><nav><a href="./">Home</a>{% for n in nav if n.route!='/' %}<a href="{{link(n.route)}}">{{n.title}}</a>{% endfor %}{% if entries|selectattr('type','equalto','product')|list %}<a href="product/">Products</a>{% endif %}{% if entries|selectattr('type','equalto','post')|list %}<a href="post/">News</a>{% endif %}</nav><a class="request" href="mailto:{{site.email or "info@example.com"}}">REQUEST A QUOTE ↗</a></div></header>
{% if blueprint_mode %}{% elif mode=='home' %}<section class="hero"><div class="wrap hero-grid"><div class="hero-text"><span class="kicker"><i></i> STRONGER CONNECTIONS. SMARTER SOLUTIONS.</span><h1>{{page.title if page.title!=site.name else 'BUILT TO LAST.'}}</h1><p>{{page.summary or site.tagline or 'Industrial excellence, quality products and dependable worldwide delivery.'}}</p><a class="button" href="product/">EXPLORE PRODUCTS <b>↗</b></a><div class="hero-tags"><span>01 / MATERIALS</span><span>02 / ENGINEERING</span><span>03 / SERVICE</span></div></div><div class="hero-art">{% if page.cover %}<img src="{{asset(page.cover)}}" alt="{{page.title}}">{% else %}<div class="ring ring-1"></div><div class="ring ring-2"></div><div class="ring ring-3"></div><span>INDUSTRY<br>IN MOTION</span>{% endif %}</div></div></section>
{% elif mode=='notfound' %}<section class="subhero"><div class="wrap"><span class="kicker">ERROR</span><h1>404 — Page not found</h1></div></section>
{% else %}<section class="subhero"><div class="wrap"><span class="kicker">{{type_names.get(page.type,'COMPANY')|upper}} / {{'PRODUCT CATALOG' if mode=='listing' else 'OUR EXPERTISE'}}</span><h1>{{page.title}}</h1><p>{{page.summary}}</p></div></section>{% endif %}
{% if blueprint_mode %}<main class="sf-canvas">{{blocks}}</main>{% else %}<main class="wrap main">{% if mode=='home' %}<div class="metrics"><div><strong>01</strong><span>PRODUCT ENGINEERING</span></div><div><strong>02</strong><span>GLOBAL SOURCING</span></div><div><strong>03</strong><span>QUALITY ASSURANCE</span></div></div>{% endif %}
{% if mode in ['home','detail'] and (content or blocks) %}<article class="rich-content">{{blocks}}{{content}}</article>{% endif %}
{% if mode=='detail' and page.cover %}<figure class="detail-image"><img src="{{asset(page.cover)}}" alt="{{page.title}}"></figure>{% endif %}
{% set cards=listing_items if mode=='listing' else entries|selectattr('type','equalto','product')|list if mode=='home' else [] %}
{% if cards %}<div class="titlebar"><div><span class="kicker">OUR PORTFOLIO</span><h2>{{'Product Catalogue' if mode=='listing' else 'Explore Our Products'}}</h2></div>{% if mode=='home' %}<a href="product/">ALL PRODUCTS ↗</a>{% endif %}</div><div class="product-grid">{% for item in cards[:12] %}<a href="{{link(item.route)}}" class="product"><div class="product-img">{% if item.cover %}<img src="{{asset(item.cover)}}" alt="{{item.title}}">{% else %}<div class="technical-graphic"><span>+</span><span>+</span><span>+</span><span>+</span></div>{% endif %}</div><div class="product-info"><span>INDUSTRIAL / {{type_names.get(item.type,'PRODUCT')|upper}}</span><h3>{{item.title}}</h3><p>{{item.summary|truncate(100)}}</p><div class="arrow">DISCOVER DETAILS <b>→</b></div></div></a>{% endfor %}</div>{% endif %}</main>
{% endif %}<footer><div class="wrap"><div class="footer-cols"><div><h2>{{site.name}}</h2><p>{{site.tagline}}</p></div><div><span>INDUSTRIAL SOLUTIONS</span><p>Quality materials. Dependable service.<br>Worldwide supply.</p></div></div><div class="bottom"><span>© {{site.name}}</span><span>DESIGNED TO DELIVER. BUILT WITH SITEFORGE.</span></div></div></footer></body></html>
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 740"><defs><linearGradient id="b" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#dae9f1"/><stop offset="1" stop-color="#aacbdc"/></linearGradient><linearGradient id="pipe" x1="0" y1="0" x2=".8" y2="1"><stop stop-color="#82b4d4"/><stop offset=".48" stop-color="#276a95"/><stop offset="1" stop-color="#124668"/></linearGradient></defs><rect width="1000" height="740" fill="url(#b)"/><g stroke="#3b7d9f" stroke-opacity=".12" stroke-width="2"><path d="M0 80h1000M0 160h1000M0 240h1000M0 320h1000M0 400h1000M0 480h1000M0 560h1000M0 640h1000M80 0v740M160 0v740M240 0v740M320 0v740M400 0v740M480 0v740M560 0v740M640 0v740M720 0v740M800 0v740M880 0v740M960 0v740"/></g><g transform="rotate(-19 530 360)"><rect x="122" y="140" width="780" height="170" rx="85" fill="url(#pipe)" stroke="#155479" stroke-width="6"/><ellipse cx="900" cy="225" rx="80" ry="85" fill="#11507b" stroke="#6fa3c4" stroke-width="9"/><ellipse cx="900" cy="225" rx="52" ry="54" fill="#b3d5e8" stroke="#0b426a" stroke-width="15"/><rect x="40" y="390" width="760" height="166" rx="82" fill="url(#pipe)" stroke="#155479" stroke-width="6"/><ellipse cx="799" cy="472" rx="77" ry="83" fill="#145784" stroke="#7eaecd" stroke-width="8"/><ellipse cx="799" cy="472" rx="51" ry="53" fill="#b3d5e8" stroke="#0b426a" stroke-width="15"/></g><path d="M52 82h180" stroke="#286c90" stroke-width="2"/><text x="52" y="67" fill="#25658a" font-size="16" font-family="Arial" letter-spacing="5">MATERIAL SYSTEMS</text></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+36
View File
@@ -0,0 +1,36 @@
/* Portable SiteForge blocks, loaded with every theme (all styles scoped to sf-). */
.sf-section{background:var(--sf-bg,transparent);color:var(--sf-fg,inherit);padding:var(--sf-pad,82px) 0;margin:0}.sf-section:nth-of-type(even){background:var(--sf-bg,#f4f6f9)}.sf-container{max-width:1200px;margin:auto;padding:0 32px}.sf-section h1,.sf-section h2,.sf-section h3,.sf-section p{color:inherit;line-height:1.3}.sf-section h1{font-size:clamp(38px,5vw,76px);font-weight:790;letter-spacing:-.052em}.sf-section h2{font-size:clamp(27px,3.2vw,44px);font-weight:760;letter-spacing:-.037em}.sf-kicker{display:block;font-size:12px;letter-spacing:.17em;text-transform:uppercase;font-weight:740;color:var(--accent,#3569d3);margin-bottom:18px}.sf-hero-grid,.sf-image-text{display:grid;grid-template-columns:1.08fr .92fr;gap:68px;align-items:center}.sf-hero-copy p{font-size:18px;line-height:1.75;max-width:520px;opacity:.75}.sf-hero-img,.sf-image-side{overflow:hidden;border-radius:9px;min-height:380px;background:linear-gradient(130deg,#d5e1ee,#eef3f9);display:grid;place-items:center}.sf-hero-img img,.sf-image-side img{width:100%;height:100%;max-height:580px;object-fit:cover}.sf-hero-abstract{font-size:110px;color:#a7bdd4}.sf-button{display:inline-flex;align-items:center;justify-content:center;gap:28px;background:var(--accent,#315ee2);color:white!important;padding:16px 26px;border-radius:6px;font-size:14px;text-decoration:none;font-weight:720;margin-top:24px}.sf-section-head{display:flex;align-items:end;justify-content:space-between;margin-bottom:32px;gap:24px}.sf-section-head h2{margin:0}.sf-section-head>a,.sf-link{color:var(--accent,#3569d3);font-weight:700;text-decoration:none}.sf-prose{font-size:16px;line-height:1.85;max-width:930px}.sf-prose p{line-height:1.85}.sf-features,.sf-collection,.sf-stats{display:grid;gap:20px}.sf-features{grid-template-columns:repeat(3,minmax(0,1fr))}.sf-feature{border:1px solid #e0e6eb;padding:32px 28px;border-radius:8px;background:#fff;color:#253247}.sf-feature>span{font-size:12px;font-weight:800;color:var(--accent,#346add)}.sf-feature h3{font-size:22px;margin:30px 0 8px}.sf-feature p{font-size:14px;line-height:1.7;color:#647181}.sf-stats{grid-template-columns:repeat(4,1fr);padding:40px 0;border-top:1px solid #b7c0cd66;border-bottom:1px solid #b7c0cd66}.sf-stats>div{display:flex;flex-direction:column;gap:8px}.sf-stats strong{font-size:clamp(28px,4vw,53px);letter-spacing:-.04em}.sf-stats span{font-size:12px;opacity:.65;letter-spacing:.04em}.sf-collection{grid-template-columns:repeat(var(--sf-cols,3),minmax(0,1fr))}.sf-item{display:block;text-decoration:none;color:#233044;background:#fff;border:1px solid #e8edf1;border-radius:8px;overflow:hidden;transition:transform .2s,box-shadow .2s}.sf-item:hover{transform:translateY(-4px);box-shadow:0 18px 38px #344d7018}.sf-item-img{height:210px;overflow:hidden;background:#e4eaf0}.sf-item-img img{height:100%;width:100%;object-fit:cover}.sf-placeholder{display:grid;place-items:center;width:100%;height:100%;min-height:190px;background:linear-gradient(130deg,#e2eaf2,#c9d7e6);font-size:72px;color:#8399b2;font-weight:800}.sf-large{min-height:360px}.sf-item-copy{padding:25px}.sf-item-copy small{text-transform:uppercase;letter-spacing:.13em;color:var(--accent,#376edc)}.sf-item-copy h3{font-size:22px;margin:12px 0 7px}.sf-item-copy p{font-size:14px;line-height:1.6;color:#7a8796;min-height:42px}.sf-item-copy span{font-size:12px;color:var(--accent,#376edc);font-weight:800}.sf-image-text>div>p{font-size:17px;line-height:1.8;max-width:520px;opacity:.7}.sf-reverse>div:first-child{order:2}.sf-gallery{display:grid;grid-template-columns:repeat(3,1fr);gap:15px}.sf-gallery img{width:100%;height:230px;object-fit:cover;border-radius:6px}.sf-faq{max-width:920px}.sf-faq details{border-bottom:1px solid #dce3eb;padding:22px 0}.sf-faq summary{font-size:17px;cursor:pointer;font-weight:700}.sf-faq p{color:#647181;line-height:1.75}.sf-cta-wrap{display:flex;justify-content:space-between;align-items:center;gap:24px}.sf-cta-wrap h2{margin:0}.sf-cta-wrap p{font-size:16px;opacity:.75}.sf-section a{cursor:pointer}.sf-muted{opacity:.65}
@media(max-width:820px){.sf-container{padding:0 20px}.sf-section{padding:50px 0}.sf-hero-grid,.sf-image-text{grid-template-columns:1fr;gap:26px}.sf-reverse>div:first-child{order:initial}.sf-hero-img,.sf-image-side{min-height:250px}.sf-features,.sf-collection{grid-template-columns:repeat(2,minmax(0,1fr))}.sf-stats{grid-template-columns:repeat(2,1fr)}.sf-cta-wrap{align-items:flex-start;flex-direction:column}}
@media(max-width:540px){.sf-features,.sf-collection,.sf-gallery{grid-template-columns:1fr}.sf-section-head{display:block}.sf-section-head>a{display:block;margin-top:13px}.sf-item-img{height:220px}.sf-hero-grid{gap:22px}.sf-hero-copy p{font-size:16px}.sf-stats{gap:28px}}
.sf-attributes{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 32px;border-top:1px solid #e5eaf0}.sf-attribute{display:flex;justify-content:space-between;align-items:center;gap:18px;border-bottom:1px solid #e5eaf0;padding:19px 10px;font-size:14px}.sf-attribute span{color:#748497}.sf-attribute strong{font-weight:720;text-align:right}@media(max-width:640px){.sf-attributes{grid-template-columns:1fr}}
/* SiteForge responsive no-code grid container */
.sf-layout-grid{display:grid;grid-template-columns:var(--sf-layout-cols,1fr 1fr);gap:var(--sf-layout-gap,24px);align-items:stretch}
.sf-layout-column{min-width:0}.sf-layout-column>.sf-section{padding:0;margin:0 0 18px}.sf-layout-column .sf-container{width:100%;padding:0}
.sf-layout-column>.sf-section:last-child{margin-bottom:0}
@media(max-width:820px){.sf-layout-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}}
@media(max-width:540px){.sf-layout-grid{grid-template-columns:minmax(0,1fr)!important}}
/* V8.6: accessible, script-free commercial design blocks */
.sf-slides{display:flex;gap:18px;overflow-x:auto;scroll-snap-type:x mandatory;padding:6px 0 18px;scrollbar-width:thin}
.sf-slide{flex:0 0 min(78%,720px);min-width:220px;margin:0;scroll-snap-align:start}
.sf-slide img{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:10px}
.sf-slide figcaption{padding:10px 2px;opacity:.7;font-size:14px}
.sf-video{aspect-ratio:16/9;max-width:1000px;background:#ebf0f5;border-radius:10px;overflow:hidden}
.sf-video iframe{width:100%;height:100%;border:0}
.sf-tab{padding:19px 0;border-bottom:1px solid #dde4ec;max-width:960px}
.sf-tab summary{font-size:18px;font-weight:700;cursor:pointer}.sf-tab p{max-width:850px;line-height:1.8}
.sf-prices{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:18px}
.sf-price{padding:28px;background:#fff;border:1px solid #dee5ef;border-radius:10px;color:#29394b}
.sf-price strong{display:block;font-size:27px;margin:14px 0}.sf-price li{margin:11px 0}.sf-price ul{padding-left:19px}
.sf-map-address{font-style:normal;font-size:18px;white-space:pre-line;line-height:1.8}
@media (prefers-reduced-motion:reduce){.sf-slides{scroll-behavior:auto}.sf-item{transition:none}}
@media(max-width:540px){.sf-slide{flex-basis:92%}}
/* V8.33: trade-pro editable block layout and accessible focus treatment.
Retain the core block contracts; keep theme rules scoped to this theme. */
.sf-theme-trade-pro .sf-canvas .sf-section{max-width:100%;min-width:0}
.sf-theme-trade-pro .sf-section img{max-width:100%;height:auto}
.sf-theme-trade-pro :is(a,button,input,select,textarea):focus-visible{outline:3px solid #2563eb;outline-offset:3px}
@media(max-width:640px){.sf-theme-trade-pro .sf-layout-column{min-width:0}.sf-theme-trade-pro .sf-canvas .sf-section{padding-inline:clamp(12px,4vw,24px)}}
+39 -3
View File
@@ -5,9 +5,45 @@
"version": "1.0.0", "version": "1.0.0",
"style": "international-b2b", "style": "international-b2b",
"entry": "pro.html", "entry": "pro.html",
"styles": ["theme.css"], "styles": [
"page_templates": ["home","about","contact","listing","category","product","article","brand","case","service","faq","standard","notfound"], "theme.css",
"blocks": ["hero","richtext","features","stats","collection","image_text","gallery","faq","cta","attributes","spacer","layout","carousel","video","tabs","pricing","map","form_link"], "blocks.css"
],
"page_templates": [
"home",
"about",
"contact",
"listing",
"category",
"product",
"article",
"brand",
"case",
"service",
"faq",
"standard",
"notfound"
],
"blocks": [
"hero",
"richtext",
"features",
"stats",
"collection",
"image_text",
"gallery",
"faq",
"cta",
"attributes",
"spacer",
"layout",
"carousel",
"video",
"tabs",
"pricing",
"map",
"form_link"
],
"responsive": true, "responsive": true,
"auto_binding": true, "auto_binding": true,
"professional_layout": true, "professional_layout": true,