[CMS-VISUAL] F2: 门户布局区块输出稳定 id + data-plb-id 锚点(display:contents 零布局影响)+ postMessage 预览监听脚本(门户侧 50 测试绿)

This commit is contained in:
chenxing
2026-09-28 12:52:45 +08:00
parent c60f9dffb7
commit 089cb54011
5 changed files with 69 additions and 3 deletions
@@ -353,10 +353,14 @@ public class PortalPageRenderer {
}
}
/** 区块公共头:type/title,并合成为 category.name 供主题 fragment 复用(block.category.name 取标题) */
/** 区块公共头:type/title,并合成为 category.name 供主题 fragment 复用(block.category.name 取标题)。
* F2(docs/20):透传区块 id(编辑器实时预览 postMessage 重排的 DOM 锚点 data-plb-id)。 */
private Map<String, Object> baseBlock(Map<String, Object> spec, String type, String fallbackTitle) {
Map<String, Object> block = new HashMap<>();
block.put("type", type);
if (spec.get("id") != null) {
block.put("id", String.valueOf(spec.get("id")));
}
String title = spec.get("title") == null ? "" : String.valueOf(spec.get("title")).trim();
if (title.isEmpty()) {
title = fallbackTitle;
@@ -30,7 +30,9 @@
注意坑④:th:replace(优先级 100)先于 th:case(300)执行——case 必须放父元素、replace 放子元素。 -->
<th:block th:if="${layoutBlocks != null}">
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: style}" />
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: previewScript}" />
<th:block th:each="block : ${layoutBlocks}">
<div class="plb-anchor" th:attr="data-plb-id=${block['id']}" style="display:contents">
<th:block th:switch="${block.type}">
<th:block th:case="'article_list'">
<th:block th:switch="${block.homeStyle}">
@@ -48,6 +50,7 @@
<th:block th:case="'notice_bar'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: notice_bar(${block})}" /></th:block>
<th:block th:case="*"><th:block th:replace="~{'themes/common/fragments/home-blocks' :: render(${block})}" /></th:block>
</th:block>
</div>
</th:block>
</th:block>
@@ -83,11 +83,17 @@
.plb-notice-warn{background:var(--plb-warn-bg);border-color:var(--plb-warn-border)}
.plb-notice-warn .plb-notice-tag{color:var(--plb-warn-fg)}
/* 全宽区块必须跨满整行:网格型主题(mint-breeze 系 .home-main 是三列)会把它们塞进单列,
非网格父容器下 grid-column 无副作用,故由骨架自带。 */
非网格父容器下 grid-column 无副作用,故由骨架自带。
F2(docs/20):布局区块经 .plb-anchor(display:contents 预览锚点)包裹时按子选择器等效覆盖。 */
main > .plb-ticker{grid-column:1 / -1}
main > .plb-newshot{grid-column:1 / -1}
main > .plb-section{grid-column:1 / -1}
main > .plb-notice-bar{grid-column:1 / -1}
main > .plb-anchor{display:contents;grid-column:auto}
main > .plb-anchor > .plb-ticker{grid-column:1 / -1}
main > .plb-anchor > .plb-newshot{grid-column:1 / -1}
main > .plb-anchor > .plb-section{grid-column:1 / -1}
main > .plb-anchor > .plb-notice-bar{grid-column:1 / -1}
@media (max-width:768px){
.plb-grid2{columns:1}
.plb-imagewall{grid-template-columns:repeat(2,1fr)}
@@ -326,3 +332,49 @@ main > .plb-notice-bar{grid-column:1 / -1}
th:text="${block['noticeText']}">公告正文</span>
</div>
</div>
<!--
F2(docs/20)实时预览监听脚本:管理端首页布局编辑器(iframe 加载正式门户)经 postMessage
发送未保存的编排模拟——{type:'cms-layout-preview', order:[区块id…], cssVars:{…}}。
行为:按 order 重排/隐藏 [data-plb-id] 锚点(display:contents 包裹层,零布局影响);
未知 id(未保存的新区块)静默忽略 → 编辑器侧提供手动刷新兜底(D-3 降级路径)。
安全:仅 DOM 移动/显隐与 CSS 变量赋值(textContent/innerHTML 一概不碰),payload 严格校形。
引用位置:主题 home.html 布局分支随 :: style 一并引入一次。
-->
<th:block th:fragment="previewScript">
<script>
(function () {
if (window.__cmsLayoutPreviewBound) { return; }
window.__cmsLayoutPreviewBound = true;
window.addEventListener('message', function (ev) {
var data = ev.data;
if (!data || data.type !== 'cms-layout-preview') { return; }
var anchors = Array.prototype.slice.call(document.querySelectorAll('[data-plb-id]'));
if (!anchors.length) { return; }
var order = Array.isArray(data.order) ? data.order.filter(function (x) {
return typeof x === 'string' && x.length <= 64;
}) : null;
if (order) {
var byId = {};
anchors.forEach(function (a) { byId[a.getAttribute('data-plb-id')] = a; });
var parent = anchors[0].parentNode;
order.forEach(function (id) {
var a = byId[id];
if (a) { a.style.display = 'contents'; parent.appendChild(a); }
});
anchors.forEach(function (a) {
if (order.indexOf(a.getAttribute('data-plb-id')) < 0) { a.style.display = 'none'; }
});
}
if (data.cssVars && typeof data.cssVars === 'object') {
var bad = /[^a-zA-Z0-9-_, .#%()]/;
Object.keys(data.cssVars).forEach(function (k) {
if (/^--[a-zA-Z][\w-]*$/.test(k) && !bad.test(String(data.cssVars[k]))) {
document.documentElement.style.setProperty(k, data.cssVars[k]);
}
});
}
});
})();
</script>
</th:block>
@@ -26,8 +26,12 @@
<!-- 首页布局模式(docs/16 §4.4):home_layout 落库后按区块序列渲染(common 兜底骨架,本主题不另设形态) -->
<th:block th:if="${layoutBlocks != null}">
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: style}" />
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: previewScript}" />
<!-- F2:data-plb-id 锚点(display:contents)供编辑器实时预览 postMessage 重排 -->
<th:block th:each="block : ${layoutBlocks}">
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: render(${block})}" />
<div class="plb-anchor" th:attr="data-plb-id=${block['id']}" style="display:contents">
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: render(${block})}" />
</div>
</th:block>
</th:block>
@@ -30,7 +30,9 @@
否则未命中的 case 分支也会求值 fragment 表达式(缺键 Map 访问直接抛错整页 500)。 -->
<th:block th:if="${layoutBlocks != null}">
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: style}" />
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: previewScript}" />
<th:block th:each="block : ${layoutBlocks}">
<div class="plb-anchor" th:attr="data-plb-id=${block['id']}" style="display:contents">
<th:block th:switch="${block.type}">
<th:block th:case="'article_list'">
<th:block th:switch="${block.homeStyle}">
@@ -48,6 +50,7 @@
<th:block th:case="'notice_bar'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: notice_bar(${block})}" /></th:block>
<th:block th:case="*"><th:block th:replace="~{'themes/common/fragments/home-blocks' :: render(${block})}" /></th:block>
</th:block>
</div>
</th:block>
</th:block>