feat(phoneimage): 三列布局重构 - 添加渲染预览区并改造渲染管线

- 增加中间渲染预览列(540px),三列布局:编辑器 | 预览 | 缩略图
- CSS作用域迁移:排版样式从#editor-text-area迁移到#render-preview
- 编辑器恢复干净默认样式,消除表格/图片间隙和溢出问题
- 新增syncPreview()实时同步编辑器内容到预览区(300ms防抖)
- captureEditorBlocks()改为从预览区DOM测高,不再克隆编辑器DOM
- render()改为从预览区读取已预处理HTML,所见即所得
This commit is contained in:
augushong
2026-05-12 23:12:48 +08:00
parent ccbfdde73e
commit 29dbc7ca55
3 changed files with 144 additions and 97 deletions

View File

@@ -747,7 +747,17 @@ body > .page-header-right .layui-btn:not(.layui-btn-primary):not(.layui-btn-norm
box-sizing: border-box; box-sizing: border-box;
} }
/* ============================================
Editor Content - Basic Readability
编辑器容器仅保留基础可读性样式
============================================ */
/* Editor container - basic readability only */
#editor-text-area { #editor-text-area {
font-family: var(--pi-font-family);
font-size: var(--pi-font-size-base);
line-height: var(--pi-line-height);
color: var(--pi-color-text);
width: 540px; width: 540px;
min-height: 300px; min-height: 300px;
padding: 10px; padding: 10px;
@@ -755,93 +765,95 @@ body > .page-header-right .layui-btn:not(.layui-btn-primary):not(.layui-btn-norm
box-sizing: border-box; box-sizing: border-box;
} }
/* Keep images constrained in editor */
#editor-text-area img {
max-width: 100%;
height: auto;
}
/* ============================================ /* ============================================
Editor Content Typography (编辑器内排版样式) Render Preview Area (渲染预览区排版样式)
与截图输出区域staging的视觉保持一致 中间预览列的完整排版样式
作用域: #editor-text-area 内部元素
============================================ */ ============================================ */
#editor-text-area { #render-preview {
font-family: var(--pi-font-family); font-family: var(--pi-font-family);
font-size: var(--pi-font-size-base); font-size: var(--pi-font-size-base);
line-height: var(--pi-line-height); line-height: var(--pi-line-height);
color: var(--pi-color-text); color: var(--pi-color-text);
} }
/* 编辑区标题字号 — 与 .page-content h2~h6 对齐 */ /* 预览区标题字号 */
#editor-text-area h1 { #render-preview h1 {
font-size: 22px; font-size: 22px;
font-weight: bold; font-weight: bold;
letter-spacing: 1px; letter-spacing: 1px;
margin-top: 24px; margin-top: 24px;
margin-bottom: 12px; margin-bottom: 12px;
color: var(--pi-color-text);
line-height: 1.4; line-height: 1.4;
} }
#editor-text-area h2 { #render-preview h2 {
font-weight: normal; font-weight: normal;
font-size: 18px; font-size: 18px;
letter-spacing: 1px; letter-spacing: 1px;
margin-top: 24px; margin-top: 24px;
margin-bottom: 12px; margin-bottom: 12px;
color: #333;
} }
#editor-text-area h3 { #render-preview h3 {
font-weight: normal; font-weight: normal;
font-size: 16px; font-size: 16px;
margin-top: 20px; margin-top: 20px;
margin-bottom: 10px; margin-bottom: 10px;
color: #555;
} }
#editor-text-area h4 { #render-preview h4 {
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
margin-top: var(--pi-spacing-sm); margin-top: 10px;
margin-bottom: 5px; margin-bottom: 5px;
} }
#editor-text-area h5 { #render-preview h5 {
font-size: 13px; font-size: 13px;
font-weight: bold; font-weight: bold;
margin-top: var(--pi-spacing-sm); margin-top: 10px;
margin-bottom: 5px; margin-bottom: 5px;
} }
#editor-text-area h6 { #render-preview h6 {
font-size: 12px; font-size: 12px;
font-weight: bold; font-weight: bold;
margin-top: var(--pi-spacing-sm); margin-top: 10px;
margin-bottom: 5px; margin-bottom: 5px;
color: var(--pi-color-text-light); color: #999;
} }
/* 编辑区段落 */ /* 预览区段落 */
#editor-text-area p { #render-preview p {
text-indent: 2em; text-indent: 2em;
margin-bottom: 16px; margin-bottom: 16px;
color: #333; color: #333;
} }
/* 编辑区图片 */ /* 预览区图片 */
#editor-text-area img { #render-preview img {
max-width: 100% !important; max-width: 100% !important;
height: auto !important; height: auto !important;
display: block; display: block;
margin: 10px 0; margin: 10px 0;
} }
/* 编辑区表格 */ /* 预览区表格 */
#editor-text-area table { #render-preview table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin: 10px 0; margin: 10px 0;
font-size: 13px; font-size: 13px;
} }
#editor-text-area th { #render-preview th {
background-color: #f0f0f0; background-color: #f0f0f0;
font-weight: bold; font-weight: bold;
padding: 8px 10px; padding: 8px 10px;
@@ -849,17 +861,17 @@ body > .page-header-right .layui-btn:not(.layui-btn-primary):not(.layui-btn-norm
text-align: left; text-align: left;
} }
#editor-text-area td { #render-preview td {
padding: 8px 10px; padding: 8px 10px;
border: 1px solid #ddd; border: 1px solid #ddd;
} }
#editor-text-area tr:nth-child(even) td { #render-preview tr:nth-child(even) td {
background-color: #f9f9f9; background-color: #f9f9f9;
} }
/* 编辑区代码块 */ /* 预览区代码块 */
#editor-text-area pre { #render-preview pre {
background: #f5f5f5; background: #f5f5f5;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
border-radius: 4px; border-radius: 4px;
@@ -870,34 +882,34 @@ body > .page-header-right .layui-btn:not(.layui-btn-primary):not(.layui-btn-norm
margin: 10px 0; margin: 10px 0;
} }
#editor-text-area code { #render-preview code {
font-family: 'Courier New', Consolas, monospace; font-family: 'Courier New', Consolas, monospace;
font-size: 13px; font-size: 13px;
} }
#editor-text-area pre code { #render-preview pre code {
background: none; background: none;
border: none; border: none;
padding: 0; padding: 0;
} }
/* 编辑区引用 */ /* 预览区引用 */
#editor-text-area blockquote { #render-preview blockquote {
border-left: 1px solid #ccc; border-left: 1px solid #ccc;
font-style: normal; font-style: normal;
color: #888; color: #888;
padding-left: 15px; padding-left: 15px;
margin: var(--pi-spacing-sm) 0; margin: 10px 0;
} }
/* 编辑区列表 */ /* 预览区列表 */
#editor-text-area ul, #render-preview ul,
#editor-text-area ol { #render-preview ol {
padding-left: var(--pi-spacing); padding-left: 20px;
margin-bottom: var(--pi-spacing-sm); margin-bottom: 10px;
} }
#editor-text-area li { #render-preview li {
margin-bottom: 5px; margin-bottom: 5px;
} }

View File

@@ -95,9 +95,10 @@ var PhoneImageEngine = (function () {
var pageHeight = sizeConfig.height; var pageHeight = sizeConfig.height;
var contentAreaHeight = pageHeight - (config.contentPadding * 2); var contentAreaHeight = pageHeight - (config.contentPadding * 2);
// 从 wangeditor 读取内容 // 从预览区读取已预处理的内容
var editorHtml = window.phoneImageEditor ? window.phoneImageEditor.getHtml() : postData.content_html; syncPreview();
var cleanHtml = preprocessContent(editorHtml); var previewEl = document.getElementById('render-preview');
var cleanHtml = previewEl ? previewEl.innerHTML : '';
var blocks = parseHtmlToBlocks(cleanHtml); var blocks = parseHtmlToBlocks(cleanHtml);
// 空内容检测 // 空内容检测
@@ -111,7 +112,7 @@ var PhoneImageEngine = (function () {
pages.push(generateCoverPage(sizeConfig)); pages.push(generateCoverPage(sizeConfig));
// 内容分页(使用 captureEditorBlocksT3 会完善截图逻辑) // 内容分页(使用 captureEditorBlocksT3 会完善截图逻辑)
captureEditorBlocks(editorHtml, blocks, contentAreaHeight, sizeConfig).then(function(contentPages) { captureEditorBlocks(cleanHtml, blocks, contentAreaHeight, sizeConfig).then(function(contentPages) {
pages = pages.concat(contentPages); pages = pages.concat(contentPages);
// 尾页 // 尾页
@@ -194,6 +195,15 @@ var PhoneImageEngine = (function () {
return html; return html;
} }
/**
* 同步渲染预览区 - 将编辑器HTML写入预览区
*/
function syncPreview() {
var html = window.phoneImageEditor ? window.phoneImageEditor.getHtml() : postData.content_html;
var cleanHtml = preprocessContent(html);
$('#render-preview').html(cleanHtml);
}
/** /**
* 将HTML解析为块级元素数组 * 将HTML解析为块级元素数组
* 每个块: { type, html, estimatedHeight } * 每个块: { type, html, estimatedHeight }
@@ -354,7 +364,6 @@ var PhoneImageEngine = (function () {
if (blocks[i].type === 'page-break') { if (blocks[i].type === 'page-break') {
if (currentGroup.blocks.length > 0) groups.push(currentGroup); if (currentGroup.blocks.length > 0) groups.push(currentGroup);
currentGroup = { blocks: [], domIndices: [] }; currentGroup = { blocks: [], domIndices: [] };
// page-break 对应 wangeditor 中的 divider(hr),在 DOM 中也是一个子元素
domIdx++; domIdx++;
continue; continue;
} }
@@ -369,70 +378,55 @@ var PhoneImageEngine = (function () {
return deferred.promise(); return deferred.promise();
} }
// 获取编辑器子元素 // 从 #render-preview 获取子元素(已预处理的干净内容)
var editorChildren = getEditorChildren(); var previewEl = document.getElementById('render-preview');
if (!previewEl) {
// 创建测量容器500px = 540 - 20*2 padding与内容区一致 // fallback: 如果预览区不存在给所有block设默认高度
var $mc = $('<div>').css({ for (var k = 0; k < blocks.length; k++) {
width: '500px', if (!blocks[k].estimatedHeight) blocks[k].estimatedHeight = 40;
position: 'fixed',
left: '-9999px',
top: '0',
visibility: 'hidden',
background: '#ffffff',
fontSize: '14px',
lineHeight: '1.8',
boxSizing: 'border-box'
});
$('body').append($mc);
// 串行测高每组
var gi = 0;
function measureNext() {
if (gi >= groups.length) {
$mc.remove();
// 给尚未设置高度的 block 设默认值
for (var k = 0; k < blocks.length; k++) {
if (!blocks[k].estimatedHeight) {
blocks[k].estimatedHeight = 40;
}
}
// 用测量的实际高度后的 blocks 调用原有 paginateContent 分页
var contentPages = paginateContent(blocks, contentAreaHeight, sizeConfig);
deferred.resolve(contentPages);
return;
} }
var contentPages = paginateContent(blocks, contentAreaHeight, sizeConfig);
deferred.resolve(contentPages);
return deferred.promise();
}
var previewChildren = Array.prototype.slice.call(previewEl.children);
// 逐组测高
for (var gi = 0; gi < groups.length; gi++) {
var group = groups[gi]; var group = groups[gi];
$mc.empty(); var groupHeight = 0;
for (var j = 0; j < group.domIndices.length; j++) { for (var j = 0; j < group.domIndices.length; j++) {
var di = group.domIndices[j]; var di = group.domIndices[j];
if (di < editorChildren.length) { if (di < previewChildren.length) {
$mc.append(editorChildren[di].cloneNode(true)); var rect = previewChildren[di].getBoundingClientRect();
groupHeight += Math.round(rect.height);
} }
} }
requestAnimationFrame(function () { if (groupHeight === 0) groupHeight = group.blocks.length * 40;
var h = Math.round($mc[0].getBoundingClientRect().height);
// 按比例分配测量的实际高度给组内各个 block // 按比例分配测量的实际高度给组内各个 block
var totalEstimated = 0; var totalEstimated = 0;
for (var k = 0; k < group.blocks.length; k++) { for (var k = 0; k < group.blocks.length; k++) {
totalEstimated += (group.blocks[k].estimatedHeight || 40); totalEstimated += (group.blocks[k].estimatedHeight || 40);
} }
for (var k = 0; k < group.blocks.length; k++) { for (var k = 0; k < group.blocks.length; k++) {
var ratio = (group.blocks[k].estimatedHeight || 40) / (totalEstimated || 1); var ratio = (group.blocks[k].estimatedHeight || 40) / (totalEstimated || 1);
group.blocks[k].estimatedHeight = Math.round(h * ratio); group.blocks[k].estimatedHeight = Math.round(groupHeight * ratio);
} }
gi++;
measureNext();
});
} }
measureNext(); // 给尚未设置高度的 block 设默认值
for (var k = 0; k < blocks.length; k++) {
if (!blocks[k].estimatedHeight) {
blocks[k].estimatedHeight = 40;
}
}
var contentPages = paginateContent(blocks, contentAreaHeight, sizeConfig);
deferred.resolve(contentPages);
return deferred.promise(); return deferred.promise();
} }
@@ -1348,6 +1342,7 @@ var PhoneImageEngine = (function () {
generateImages: generateImages, generateImages: generateImages,
saveImages: saveImages, saveImages: saveImages,
saveConfig: saveConfig, saveConfig: saveConfig,
syncPreview: syncPreview,
setPageAlignment: setPageAlignment, setPageAlignment: setPageAlignment,
exportLongImage: exportLongImage, exportLongImage: exportLongImage,
getContentHtml: function () { getContentHtml: function () {

View File

@@ -70,6 +70,31 @@
background: #fff; background: #fff;
} }
.render-preview-area {
width: 540px;
overflow-y: auto;
border-right: 1px solid #e8e8e8;
background: #fff;
flex-shrink: 0;
}
.render-preview-area .preview-header {
padding: 8px 15px;
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
font-size: 13px;
color: #999;
position: sticky;
top: 0;
z-index: 5;
}
#render-preview {
min-height: 300px;
padding: 10px;
box-sizing: border-box;
}
.paginated-preview-area { .paginated-preview-area {
flex: 1; flex: 1;
overflow-x: auto; overflow-x: auto;
@@ -137,6 +162,12 @@
<div id="editor-text-area"></div> <div id="editor-text-area"></div>
</div> </div>
<!-- 中间:渲染预览区 -->
<div class="render-preview-area">
<div class="preview-header">渲染预览</div>
<div id="render-preview"></div>
</div>
<!-- 右侧:分页排版预览 --> <!-- 右侧:分页排版预览 -->
<div class="paginated-preview-area"> <div class="paginated-preview-area">
<div id="paginated-preview" class="preview-thumbnails"></div> <div id="paginated-preview" class="preview-thumbnails"></div>
@@ -214,6 +245,12 @@
autoSaveTimer = setTimeout(function () { autoSaveTimer = setTimeout(function () {
doAutoSave(); doAutoSave();
}, 2600); }, 2600);
// 预览同步300ms 防抖(独立于自动保存)
clearTimeout(window._previewSyncTimer);
window._previewSyncTimer = setTimeout(function () {
PhoneImageEngine.syncPreview();
}, 300);
}; };
// 粘贴处理:处理外部图片下载 // 粘贴处理:处理外部图片下载
@@ -289,6 +326,9 @@
PhoneImageEngine.init(postData, initConfig); PhoneImageEngine.init(postData, initConfig);
// 初始同步预览区
PhoneImageEngine.syncPreview();
// ========== 设置弹框 ========== // ========== 设置弹框 ==========
$('#btn-settings').click(function () { $('#btn-settings').click(function () {
var settingsHtml = '<div class="settings-form" style="padding:20px 20px 0;">'; var settingsHtml = '<div class="settings-form" style="padding:20px 20px 0;">';