diff --git a/app/static/workspace.js b/app/static/workspace.js index d7b4879..55df7aa 100644 --- a/app/static/workspace.js +++ b/app/static/workspace.js @@ -7,7 +7,7 @@ const typeName={page:'页面',product:'产品',vehicle:'车型',post:'新闻',br const fmtDate=t=>{if(!t)return '—';const d=new Date(t);return Number.isNaN(d.getTime())?String(t).slice(0,16):d.toLocaleDateString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit'})}; const sfSiteKey='siteforge:selected-site'; const sfRemember=id=>{try{if(id)localStorage.setItem(sfSiteKey,id)}catch{}}; -const state={launchDeploying:false,releaseRequestSeq:0,releaseRequests:[],releaseRequestsKnown:false,launchBuildId:'',launchTarget:'staging',launchReview:false,launchPreflight:null,sites:[],site:null,entries:[],builds:[],jobs:[],view:'home',csrf:'',search:'',typ:'',loading:false,job:null,selectionToken:0,building:false,delivery:{jobs:[],history:[],configured:{}},language:{group:null,sites:[],units:[]},languageGroups:[],languageEntries:[]}; +const state={launchDeploying:false,releaseRequestSeq:0,releaseRequests:[],releaseRequestsKnown:false,launchBuildId:'',launchTarget:'staging',launchReview:false,launchPreflight:null,sites:[],site:null,entries:[],builds:[],jobs:[],view:'home',csrf:'',search:'',typ:'',page:1,loading:false,job:null,selectionToken:0,building:false,delivery:{jobs:[],history:[],configured:{}},language:{group:null,sites:[],units:[]},languageGroups:[],languageEntries:[]};const PAGE_SIZE=20; const headings={home:'工作台',sites:'我的网站',guide:'建站操作指引',launch:'上线操作中心',overview:'网站概览',content:'内容与页面',design:'外观与设计',import:'导入与迁移',build:'生成与预览',languages:'多语言管理',settings:'网站设置'}; async function api(url,method='GET',data=null,extraHeaders={}){ const headers={...extraHeaders};if(method!=='GET'&&method!=='HEAD')headers['X-SiteForge-CSRF']=state.csrf; @@ -118,15 +118,24 @@ function launch(){ } function content(){if(!currentSite())return noSite();let arr=state.entries.filter(e=>(!state.typ||e.type===state.typ)&&(!state.search||`${e.title} ${e.route}`.toLowerCase().includes(state.search.toLowerCase()))); + const totalPages=Math.max(1,Math.ceil(arr.length/PAGE_SIZE)); + if(state.page>totalPages)state.page=1; + const start=(state.page-1)*PAGE_SIZE; + const pageItems=arr.slice(start,start+PAGE_SIZE); + const typeCounts={};state.entries.forEach(e=>typeCounts[e.type]=(typeCounts[e.type]||0)+1); + const tabs=[{key:'',label:'全部',count:state.entries.length},...Object.entries(typeName).filter(([k])=>typeCounts[k]).map(([key,value])=>({key,label:value,count:typeCounts[key]}))]; + const tabHtml=tabs.map(t=>``).join(''); + const pagination=totalPages>1?`
第 ${state.page} / ${totalPages} 页 · 共 ${arr.length} 条
`:''; return htmlHeading('内容与页面','所有网页、产品、车型和新闻都在这里维护,保存后重新构建即可更新网站。',actionButton('+ 添加内容','new-entry'))+ - `

内容列表

共 ${state.entries.length} 条内容。点击“设计页面”可进行可视化排版。

`+ - (arr.length?`
${arr.slice(0,200).map(e=>``).join('')}
名称内容类型页面地址状态操作
${esc(e.title)}${esc(typeName[e.type]||e.type)}${esc(e.route)}${e.status==='draft'?'草稿':'已保存'} 设计页面 ↗
`:emptyState('没有找到内容','可以清除筛选条件,或者添加新内容。',actionButton('+ 添加内容','new-entry')))+ - `${arr.length>200?'

当前显示前 200 条,请使用搜索缩小范围。

':''}
`; + `

内容列表

共 ${state.entries.length} 条内容。点击”设计页面”可进行可视化排版。

${tabHtml}
`+ + (pageItems.length?`
${pageItems.map(e=>``).join('')}
名称内容类型页面地址状态操作
${esc(e.title)}${esc(typeName[e.type]||e.type)}${esc(e.route)}${e.status==='draft'?'草稿':'已保存'} 设计页面 ↗
${pagination}`:emptyState('没有找到内容','可以清除筛选条件,或者添加新内容。',actionButton('+ 添加内容','new-entry')))+ + `
`; } function design(){if(!currentSite())return noSite();const s=state.site;const cards=Object.entries(themeName).map(([id,name])=>`
${name}

${name} ${s.theme===id?'当前使用':''}

${({automotive:'适合汽车出口、车型及品牌展示',industrial:'适合制造业、产品与技术参数',corporate:'适合集团公司、服务和案例',minimal:'适合作品展示和品牌形象','trade-pro':'适合外贸 B2B 英文站点与询盘'})[id]}

预览页面效果
`).join(''); return htmlHeading('外观与设计','先选一套网站风格,再打开可视化编辑器修改图文、导航和手机端布局。')+ `

第一步 · 选择行业主题

更换主题不会删除您已经录入的业务内容和 URL。

查看完整主题预览 ↗
${cards}
`+ - `

第二步 · 设计网页

挑选要修改的页面;可在设计器中编辑导航、页眉、页脚及各设备布局。

选择要设计的页面

修改先保存为草稿,正式发布设计后才会参与下一次静态构建。

${state.entries.filter(e=>e.type==='page').slice(0,8).map(e=>`▤

${esc(e.title)}

${esc(e.route)}

›
`).join('')||'
尚无页面,请先添加内容。
'}
提示:网站设置用于公司信息和域名;可视化编辑器用于调整页面外观。两者不会混在同一表单里。
`; + `

第二步 · 导航菜单

自定义网站的顶部导航:添加、删除、排序菜单项,支持三级下拉。

`+ + `

第三步 · 设计网页

挑选要修改的页面;可在设计器中编辑导航、页眉、页脚及各设备布局。

选择要设计的页面

修改先保存为草稿,正式发布设计后才会参与下一次静态构建。

${state.entries.filter(e=>e.type==='page').slice(0,8).map(e=>`▤

${esc(e.title)}

${esc(e.route)}

›
`).join('')||'
尚无页面,请先添加内容。
'}
提示:网站设置用于公司信息和域名;可视化编辑器用于调整页面外观。两者不会混在同一表单里。
`; } function imports(){if(!currentSite())return noSite();return htmlHeading('导入与迁移','根据您已有的资料选择操作方式:Excel 产品、WordPress 网站,或者外部 API / 数据库。')+ `
▤

导入 Excel / CSV

推荐先预览字段、检查冲突,再确认提交。

安全导入与批次回滚 →
⇄

迁移 WordPress 旧站

迁移会话、文章采集、图片与 SEO 检查。

打开迁移工作台 ↗
⌘

API / XML / MySQL

连接外部业务系统,进行字段映射和分批导入。

进入高级导入 ↗
`+ @@ -303,7 +312,7 @@ async function init(){try{const me=await api('/api/auth/me');state.csrf=me.csrf; else {state.view=target==='sites'?'sites':'home';rerender();rememberView(true)} }catch(e){notifyError(e);$('#mainContent').innerHTML=emptyState('后台加载失败',e.message,'')}} // Delegated UI interactions keep navigation and form actions centrally auditable. -document.addEventListener('click',async ev=>{const sidebar=ev.target.closest('.nav-item[data-view]');if(sidebar&&!sidebar.hasAttribute('data-action')){navigate(sidebar.dataset.view);return;}const control=ev.target.closest('[data-action]');if(!control)return;const action=control.dataset.action;try{if(action==='navigate')navigate(control.dataset.view);if(action==='language-refresh')await loadLanguage();if(action==='new-site')openCreateWizard();if(action==='select-site')await selectSite(control.dataset.id);if(action==='new-entry')openEntry();if(action==='edit-entry')openEntry(control.dataset.id);if(action==='set-theme')await setTheme(control.dataset.theme);if(action==='build-now')await buildNow();if(action==='refresh-builds')await refreshBuilds();if(action==='check-readiness')await checkReadiness();if(action==='cancel-job')await cancelJob(control.dataset.id);if(action==='report')await showReport(control.dataset.id);if(action==='release-preflight')await releasePreflight();if(action==='release-approval')await releaseApproval();if(action==='release-deploy')await releaseDeploy();if(action==='release-rollback')await releaseRollback(control.dataset.target,control.dataset.id);if(action==='launch-refresh')await launchRefresh();if(action==='launch-build')await buildNow();if(action==='launch-report')await launchReport(control.dataset.id);if(action==='launch-request-approval')await launchRequestApproval();if(action==='launch-preflight')await launchPreflight();if(action==='launch-deploy')await launchDeploy()}catch(e){notifyError(e)}}); +document.addEventListener('click',async ev=>{const sidebar=ev.target.closest('.nav-item[data-view]');if(sidebar&&!sidebar.hasAttribute('data-action')){navigate(sidebar.dataset.view);return;}const control=ev.target.closest('[data-action]');if(!control)return;const action=control.dataset.action;try{if(action==='navigate')navigate(control.dataset.view);if(action==='language-refresh')await loadLanguage();if(action==='set-type-tab'){state.typ=control.dataset.type;state.page=1;rerender()}if(action==='page-prev'){state.page=Math.max(1,state.page-1);rerender()}if(action==='page-next'){state.page++;rerender()}if(action==='new-site')openCreateWizard();if(action==='select-site')await selectSite(control.dataset.id);if(action==='new-entry')openEntry();if(action==='edit-entry')openEntry(control.dataset.id);if(action==='set-theme')await setTheme(control.dataset.theme);if(action==='build-now')await buildNow();if(action==='refresh-builds')await refreshBuilds();if(action==='check-readiness')await checkReadiness();if(action==='cancel-job')await cancelJob(control.dataset.id);if(action==='report')await showReport(control.dataset.id);if(action==='release-preflight')await releasePreflight();if(action==='release-approval')await releaseApproval();if(action==='release-deploy')await releaseDeploy();if(action==='release-rollback')await releaseRollback(control.dataset.target,control.dataset.id);if(action==='launch-refresh')await launchRefresh();if(action==='launch-build')await buildNow();if(action==='launch-report')await launchReport(control.dataset.id);if(action==='launch-request-approval')await launchRequestApproval();if(action==='launch-preflight')await launchPreflight();if(action==='launch-deploy')await launchDeploy()}catch(e){notifyError(e)}}); document.addEventListener('click',ev=>{const btn=ev.target.closest('[data-close]');if(btn)closeModal(btn.dataset.close)}); $('#siteForm').addEventListener('submit',saveSite);$('#entryForm').addEventListener('submit',saveEntry); document.addEventListener('submit',ev=>{if(ev.target.id==='settingsForm')saveSettings(ev);if(ev.target.id==='importForm')importFile(ev);if(ev.target.id==='langGroupForm')submitLanguageGroup(ev);if(ev.target.id==='langJoinForm')submitLanguageJoin(ev);if(ev.target.id==='langUnitForm')submitLanguageUnit(ev);if(ev.target.id==='langLinkForm')submitLanguageLink(ev)}); diff --git a/tests/test_theme_registry.py b/tests/test_theme_registry.py index 1ca5cf9..86374d3 100644 --- a/tests/test_theme_registry.py +++ b/tests/test_theme_registry.py @@ -182,11 +182,13 @@ def test_trade_pro_home_layout_structure(factory): def test_trade_pro_product_layout_structure(factory): - # Task 10b:product.html 移植 cms 产品详情设计([slug].astro)后的结构契约。 + # Task 12a:product.html 按 cms [slug].astro 精确对齐重做后的结构契约。 # 构建流(/preview/ HTTP)+ 带 fields/gallery 的产品条目驱动断言: - # 画廊(主图封面 + data-gallery-image 缩略图,复用平台 pro.js 增强钩子)、 - # 标题区、参数表渲染 fields 键值(品牌等管理字段不进表)、询盘 CTA、 - # 相关产品复用首页卡片网格;旧骨架 .pro-product-layout/.pro-specs 无残留。 + # hero 画廊(pro.js 钩子)+ 标题区(cms 产品页无 h1,产品名 h2 量级); + # sticky 选项卡导航 + 面板(overview 恒有 / specifications 有数据才显); + # 无 JS 平铺回退(静态标记不带 tabs-enabled、面板无内联隐藏); + # rich-body 正文表格样式 + spec-table 两对键值一行 + 面板标题品牌色下划短线; + # 询盘/相关产品沿用既有实现;旧骨架 .pro-product-layout/.pro-specs 无残留。 sid = site(factory) assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code == 200 made = factory.post(f'/api/sites/{sid}/entries', json={ @@ -210,26 +212,65 @@ def test_trade_pro_product_layout_structure(factory): assert media is not None and 'hp-200.jpg' in media.get('src', '') thumbs = soup.select('.sf-tp-product-thumbs [data-gallery-image]') assert len(thumbs) == 2 - # 标题区(唯一 h1)+ 询盘 CTA 指向联系页(/preview/ 响应会把根相对 - # href 重写为 /preview/{sid}/{bid}/contact/,故用后缀匹配) - assert soup.select_one('.sf-tp-product-summary h1.sf-tp-product-title') + # 标题区:cms 对齐——产品名 h2(2xl 量级,cms 产品页全页无 h1)+ 询盘 CTA + # (/preview/ 响应会把根相对 href 重写为 /preview/{sid}/{bid}/contact/,故用后缀匹配) + assert soup.select_one('.sf-tp-product-summary h2.sf-tp-product-title') + assert not soup.find_all('h1') # cms [slug].astro 产品页标题层级从 h2 起 assert soup.select_one('.sf-tp-product-actions a.sf-tp-btn[href$="/contact/"]') - # 参数表:fields 键值渲染(下划线转空格 + 首字母大写),空表整段隐藏 + # 选项卡导航:sticky 容器 + 单行横滚 tabs-row;首个 active,第二个有 specs 才显 + tabs = soup.select('.sf-tp-product-tabs .sf-tp-tabs-row .sf-tp-tab') + assert len(tabs) == 2 + assert 'Product Details' in tabs[0].get_text() and 'Specifications' in tabs[1].get_text() + assert tabs[0].get('data-panel') == 'overview' and tabs[1].get('data-panel') == 'specifications' + assert tabs[0].get('aria-selected') == 'true' and tabs[1].get('aria-selected') == 'false' + # 面板结构:overview 恒有(正文)+ specifications(specs 数据);首面板 active + panels = soup.select('.sf-tp-product-tabbed .sf-tp-panel') + assert len(panels) == 2 + assert panels[0].get('id') == 'overview' and 'active' in panels[0].get('class', []) + assert panels[1].get('id') == 'specifications' + # 无 JS 平铺回退:tabs-enabled 只由脚本运行时添加(静态标记不带), + # 面板无内联 display 隐藏;panel-title 保留在标记中(由 CSS 类控制隐藏) + tabbed_el = soup.select_one('[data-sf-tp-tabbed]') + assert tabbed_el is not None and 'sf-tp-tabs-enabled' not in (tabbed_el.get('class') or []) + assert all(p.get('style') is None for p in panels) + assert soup.select_one('#overview .sf-tp-panel-title') is not None + assert soup.select_one('#specifications .sf-tp-panel-title') is not None + # 切换脚本在场(cms 尾部脚本移植:容器加 sf-tp-tabs-enabled + 面板 active 切换) + assert "classList.add('sf-tp-tabs-enabled')" in html + # 正文面板:rich-body 容器(WP 规格表就在正文里——不拆表,整体渲染) + assert soup.select_one('#overview .sf-tp-product-body') is not None + # 参数表:cms spec-table 每行两对键值(2 条 specs → 1 行 2 th + 2 td); + # fields 键值渲染(下划线转空格 + 首字母大写),品牌等管理字段不进表 spec_table = soup.select_one('table.sf-tp-spec-table') assert spec_table is not None + rows = spec_table.select('tbody tr') + assert len(rows) == 1 + assert len(rows[0].select('th')) == 2 and len(rows[0].select('td')) == 2 spec_text = spec_table.get_text() assert 'Pressure' in spec_text and '210 bar' in spec_text and '500 km' in spec_text - assert 'Quanta' not in spec_text # brand 等管理字段不进参数表 + assert 'Quanta' not in spec_text # 相关产品:复用 sf-tp-card-grid;自身不出现在相关列表 cards = soup.select('.sf-tp-card-grid .sf-tp-card') assert cards and any('Companion Valve' in c.get_text() for c in cards) assert all('Hydraulic Pump' not in c.get_text() for c in cards) # 询盘板块存在(左联系信息 + 右 CTA/表单位) assert soup.select_one('.sf-tp-product-quote') is not None + # CSS 契约(pro.css 12a 块):sticky 选项卡 / 无 JS 平铺回退 / 品牌色下划短线 / + # rich-body 表格覆盖内联宽高 / spec-table 移动端横滚 + pro_css = (ROOT / 'themes' / 'trade-pro' / 'pro.css').read_text(encoding='utf-8') + for needle in ( + '.sf-tp-product-tabs{', + 'sf-tp-tabs-enabled .sf-tp-panel{display:none}', + 'sf-tp-tabs-enabled .sf-tp-panel.active{display:block}', + 'sf-tp-tabs-enabled .sf-tp-panel-title{display:none}', + '.sf-tp-detail-section > .wrap > h2{', + 'width:100% !important', + 'height:auto !important', + '.sf-tp-spec-table{min-width:720px}', + ): + assert needle in pro_css, needle # 旧骨架清理:无 .pro-product-layout/.pro-specs 残留类依赖 assert 'pro-product-layout' not in html and 'pro-specs' not in html - assert len(soup.find_all('h1')) == 1 - def test_trade_pro_listing_layout_structure(factory): # Task 10c:listing.html(category.html 同构复用)移植 cms 列表设计后的结构契约。 diff --git a/themes/trade-pro/pages/product.html b/themes/trade-pro/pages/product.html index c9e340c..a2abeaa 100644 --- a/themes/trade-pro/pages/product.html +++ b/themes/trade-pro/pages/product.html @@ -1,145 +1,187 @@ -{# trade-pro product detail — cms 产品详情设计移植(Task 10b)。 - 区块序列对照 D:/project/cms/runtimes/astro-site-runtime/src/pages/[locale]/products/[slug].astro - (像素基准 dist/hesupipe/en/products/alloy-resin-system-window/): - 面包屑 → 画廊(主图 + 单行横滚缩略图)+ 标题区(眉题/h1/导语/品牌行/ - CTA 对)→ 正文 Product Details → 参数表 Specifications(fields 键值, - 空则整段隐藏)→ 相关产品(卡片网格)→ 询盘板块(左联系信息 + 右 CTA/ - 外部表单)。 - 与 cms 的两处结构决策: - 1. Tab 导航平铺:cms 的 Tab 序列承载每产品自定义区块,平台正文是单一 - 富文本 + 参数表,仅两个面板不值得引入 JS Tab;cms 无 JS 回退形态 - 本就是面板平铺,这里固定为平铺(无 JS 依赖、静态可抓取)。 - 2. 画廊交互复用平台 pro.js 既有钩子(.pro-product-media / - .pro-gallery-thumbs / [data-gallery-image]):缩略图点击换主图 + - 主图灯箱,零新增 JS;无 JS 时全部图片仍平铺可读。无图回退封面, - 再无则占位渐变(空值不破版)。 #} +{# trade-pro product detail — cms 产品详情精确对齐重做(Task 12a,重做 10b)。 +设计源(唯一权威依据)D:/project/cms/runtimes/astro-site-runtime/src/pages/ +[locale]/products/[slug].astro,区块序列逐块对照: +detail-hero(面包屑 + 1.08/0.92 双栏:画廊/标题区,cms 产品名 h2 全页无 h1) +→ product-tabbed(sticky 选项卡:Product Details 正文恒有 + Specifications +参数表有数据才显)→ related-detail(sf-tp-card 卡片网格)→ quote-section +(左联系信息 + 右 CTA/外部表单)。 +与 cms 的三处结构决策: +1. 选项卡系统照搬 cms:容器 data-sf-tp-tabbed + .sf-tp-panel/.active 面板 + +尾部内联 {% set tp_related = (groups['product'] + groups['vehicle']) | rejectattr('route', 'equalto', page.route) | list %} {% if tp_related %}
-
-
-
- RELATED PRODUCTS -

You may also need

-
- Browse all products {{ icon('arrow-right', cls='sf-tp-icon--sm') }} -
-
- {% for tp_item in tp_related[:4] %} - - {% endfor %} -
-
+
+
+
+RELATED PRODUCTS +

You may also need

+
+Browse all products {{ icon('arrow-right', cls='sf-tp-icon--sm') }} +
+
+{% for tp_item in tp_related[:4] %} + +{% endfor %} +
+
{% endif %}
-
-
- REQUEST A QUOTE -

Inquire about this product

-

Send the specifications you need for {{ page.title }} and our trade desk replies with pricing, lead time and export packaging within one business day.

-
- {% if footer_email %} -
- - -
- {% endif %} -
- -
Direct sourcing desk

One contact from inquiry to container loading.

-
-
-
-
- {% if form_endpoint %} -

Get a quote for {{ page.title }}

-
- - - - - -

Submissions are handled by the external form service configured by the site administrator.

-
- {% else %} -

Get a quote for {{ page.title }}

-

Share your specifications and target market through the contact page; our trade desk replies within one business day.

- Contact us {{ icon('arrow-right') }} - {% endif %} -
-
+
+
+REQUEST A QUOTE +

Inquire about this product

+

Send the specifications you need for {{ page.title }} and our trade desk replies with pricing, lead time and export packaging within one business day.

+
+{% if footer_email %} +
+ + +
+{% endif %} +
+ +
Direct sourcing desk

One contact from inquiry to container loading.

+
+
+
+
+{% if form_endpoint %} +

Get a quote for {{ page.title }}

+
+ + + + + +

Submissions are handled by the external form service configured by the site administrator.

+
+{% else %} +

Get a quote for {{ page.title }}

+

Share your specifications and target market through the contact page; our trade desk replies within one business day.

+Contact us {{ icon('arrow-right') }} +{% endif %} +
+