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

View File

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

View File

@@ -70,6 +70,31 @@
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 {
flex: 1;
overflow-x: auto;
@@ -137,6 +162,12 @@
<div id="editor-text-area"></div>
</div>
<!-- 中间:渲染预览区 -->
<div class="render-preview-area">
<div class="preview-header">渲染预览</div>
<div id="render-preview"></div>
</div>
<!-- 右侧:分页排版预览 -->
<div class="paginated-preview-area">
<div id="paginated-preview" class="preview-thumbnails"></div>
@@ -214,6 +245,12 @@
autoSaveTimer = setTimeout(function () {
doAutoSave();
}, 2600);
// 预览同步300ms 防抖(独立于自动保存)
clearTimeout(window._previewSyncTimer);
window._previewSyncTimer = setTimeout(function () {
PhoneImageEngine.syncPreview();
}, 300);
};
// 粘贴处理:处理外部图片下载
@@ -289,6 +326,9 @@
PhoneImageEngine.init(postData, initConfig);
// 初始同步预览区
PhoneImageEngine.syncPreview();
// ========== 设置弹框 ==========
$('#btn-settings').click(function () {
var settingsHtml = '<div class="settings-form" style="padding:20px 20px 0;">';