From 404f4d8a2270398455fcf2556ada403b5d535056 Mon Sep 17 00:00:00 2001 From: augushong Date: Sun, 3 May 2026 23:32:41 +0800 Subject: [PATCH] =?UTF-8?q?refactor(phone-image):=20=E5=9C=A8=E4=B8=AD?= =?UTF-8?q?=E9=97=B4=E6=A0=8F=E6=88=AA=E5=9B=BE=E8=BD=AC=E6=8D=A2=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E5=92=8C=E4=BB=A3=E7=A0=81=E5=9D=97=E4=B8=BA=E5=9B=BE?= =?UTF-8?q?=E7=89=87=EF=BC=8C=E7=A1=AE=E4=BF=9D=E6=B8=B2=E6=9F=93=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 convertFlowBlocksToImages():在 #content-flow 中用 html2canvas 截图转换 table/pre 块为 img - 修改 render() 管线:renderContentFlow -> convertFlowBlocksToImages -> measureBlockHeights - 删除旧的 convertCodeBlocks() 和 convertTables()(doCapturePages 中不再需要) - 添加 parseHtmlToBlocks 的 case 'pre' 分支 - splitOversizedBlock 添加 pre 不拆分规则 - isPureImagePage 跳过 data-converted=true 的图片避免误判 --- public/static/js/phone-image.js | 251 ++++++++++++++------------------ 1 file changed, 109 insertions(+), 142 deletions(-) diff --git a/public/static/js/phone-image.js b/public/static/js/phone-image.js index a9a97f9..4fc9b09 100644 --- a/public/static/js/phone-image.js +++ b/public/static/js/phone-image.js @@ -116,37 +116,43 @@ var PhoneImageEngine = (function () { // 先渲染内容流(DOM) - 用于实测高度 renderContentFlow(blocks); - // DOM实测高度 - 等待一帧确保渲染完成 + // 等待一帧确保中间栏渲染完成 requestAnimationFrame(function () { - measureBlockHeights(blocks); + // 在中间栏中将表格和代码块截图转为图片 + convertFlowBlocksToImages(blocks).then(function () { + // 再等一帧让替换后的img渲染 + requestAnimationFrame(function () { + measureBlockHeights(blocks); - // 封面页 - pages.push(generateCoverPage(sizeConfig)); + // 封面页 + pages.push(generateCoverPage(sizeConfig)); - // 内容分页(使用实测高度) - var contentPages = paginateContent(blocks, contentAreaHeight, sizeConfig); - pages = pages.concat(contentPages); + // 内容分页(使用实测高度) + var contentPages = paginateContent(blocks, contentAreaHeight, sizeConfig); + pages = pages.concat(contentPages); - // 尾页 - pages.push(generateSummaryPage(sizeConfig, pages.length)); + // 尾页 + pages.push(generateSummaryPage(sizeConfig, pages.length)); - // 两遍遍历: 现在知道了总页数, 给每页补上 N/M 页码 - var totalPages = pages.length; - for (var i = 0; i < pages.length; i++) { - if (pages[i].type === 'content') { - // 在页脚中添加 N/M 格式页码 - pages[i].html = pages[i].html.replace( - '' + pages[i].pageNum + '', - '' + (i + 1) + '/' + totalPages + '' - ); - } - } + // 两遍遍历: 现在知道了总页数, 给每页补上 N/M 页码 + var totalPages = pages.length; + for (var i = 0; i < pages.length; i++) { + if (pages[i].type === 'content') { + // 在页脚中添加 N/M 格式页码 + pages[i].html = pages[i].html.replace( + '' + pages[i].pageNum + '', + '' + (i + 1) + '/' + totalPages + '' + ); + } + } - // 渲染缩略图(右侧)- 异步截图 - renderThumbnails(sizeConfig).then(function () { - deferred.resolve(pages); - }).catch(function (err) { - deferred.reject(err); + // 渲染缩略图(右侧)- 异步截图 + renderThumbnails(sizeConfig).then(function () { + deferred.resolve(pages); + }).catch(function (err) { + deferred.reject(err); + }); + }); }); }); @@ -273,6 +279,12 @@ var PhoneImageEngine = (function () { block.estimatedHeight = 0; block.needsMeasurement = true; break; + case 'pre': + block.type = 'pre'; + block.html = $el[0].outerHTML; + block.estimatedHeight = 0; + block.needsMeasurement = true; + break; case 'img': // 尝试从data属性计算比例高度, 否则标记需要DOM测量 var imgW = parseInt($el.attr('data-original-width'), 10); @@ -483,6 +495,11 @@ var PhoneImageEngine = (function () { return [block]; } + // 代码块不拆分,保留原样 + if (block.type === 'pre') { + return [block]; + } + // 文本类块:按句子拆分 var text = ''; var wrapperTag = 'p'; @@ -666,112 +683,6 @@ var PhoneImageEngine = (function () { return { type: 'summary', html: html }; } - // ===== DOM渲染 ===== - - /** - * 将 #render-staging 中的代码块转为图片 - * 使用 prismjs 高亮 + html2canvas 截图替换 - * @returns {jQuery Deferred} resolves when all code blocks are converted - */ - function convertCodeBlocks() { - var deferred = $.Deferred(); - var $staging = $('#render-staging'); - - // 检查 Prism 是否可用 - if (typeof Prism === 'undefined') { - deferred.resolve(); - return deferred.promise(); - } - - // 给没有语言类的代码块添加默认语言 - $staging.find('pre > code:not([class*="language-"])').addClass('language-plaintext'); - - // 高亮所有代码块 - $staging.find('pre code').each(function () { - Prism.highlightElement(this); - }); - - // 逐个截图替换,每次重新查找避免 replaceWith 后索引失效 - function convertNext() { - var $pre = $staging.find('pre').first(); - if ($pre.length === 0) { - deferred.resolve(); - return; - } - - html2canvas($pre[0], { - scale: 2, - useCORS: true, - backgroundColor: '#f5f5f5', - logging: false - }).then(function (canvas) { - var imgData = canvas.toDataURL('image/jpeg', 0.92); - var $img = $(''); - $img.attr('src', imgData); - $img.css({ - maxWidth: '100%', - height: 'auto', - display: 'block', - margin: '10px 0' - }); - $pre.replaceWith($img); - convertNext(); - }).catch(function () { - // 截图失败则跳过当前元素继续处理下一个 - $pre.remove(); - convertNext(); - }); - } - - convertNext(); - return deferred.promise(); - } - - /** - * 将 #render-staging 中的表格转为图片 - * 使用 html2canvas 截图替换(与 convertCodeBlocks 模式相同) - * @returns {jQuery Deferred} resolves when all tables are converted - */ - function convertTables() { - var deferred = $.Deferred(); - var $staging = $('#render-staging'); - - // 逐个截图替换,每次重新查找避免 replaceWith 后索引失效 - function convertNext() { - var $table = $staging.find('.page-content table').first(); - if ($table.length === 0) { - deferred.resolve(); - return; - } - - html2canvas($table[0], { - scale: 2, - useCORS: true, - backgroundColor: '#ffffff', - logging: false - }).then(function (canvas) { - var imgData = canvas.toDataURL('image/jpeg', 0.92); - var $img = $(''); - $img.attr('src', imgData); - $img.css({ - maxWidth: '100%', - height: 'auto', - display: 'block', - margin: '10px 0' - }); - $table.replaceWith($img); - convertNext(); - }).catch(function () { - // 截图失败则跳过当前元素继续处理下一个 - $table.remove(); - convertNext(); - }); - } - - convertNext(); - return deferred.promise(); - } - // ===== 纯图片页优化 ===== /** @@ -794,6 +705,9 @@ var PhoneImageEngine = (function () { var $imgs = $content.find('img'); if ($imgs.length !== 1) return false; + // 跳过由表格/代码块转换而来的图片(不应被当作纯图片页处理) + if ($imgs.first().attr('data-converted') === 'true') return false; + // img必须有有效src var src = $imgs.first().attr('src'); return src && src.length > 0; @@ -876,17 +790,8 @@ var PhoneImageEngine = (function () { requestAnimationFrame(function () { // html2canvas会跳过visibility:hidden的元素,临时切换为可见 $staging.css({ visibility: 'visible' }); - // 先转换代码块为图片 - convertCodeBlocks().then(function () { - return convertTables(); - }).then(function () { - runCaptureLoop($staging, opts, deferred); - }).catch(function () { - // 代码块转换失败,继续转换表格并截图流程 - convertTables().then(function () { - runCaptureLoop($staging, opts, deferred); - }); - }); + // 表格和代码块已在中间栏中转为图片,无需再次转换 + runCaptureLoop($staging, opts, deferred); }); }); @@ -1062,6 +967,68 @@ var PhoneImageEngine = (function () { }); } + /** + * 在中间栏 #content-flow 中将表格和代码块截图转为图片 + * 在 renderContentFlow(blocks) 之后、measureBlockHeights(blocks) 之前调用 + * @param {Array} blocks - parseHtmlToBlocks 返回的块数组 + * @returns {jQuery Deferred} resolves with blocks array + */ + function convertFlowBlocksToImages(blocks) { + var deferred = $.Deferred(); + var $flow = $('#content-flow'); + + function convertNextBlock(blockIdx) { + // 找到下一个需要转换的 block + while (blockIdx < blocks.length) { + if (blocks[blockIdx].type === 'table' || blocks[blockIdx].type === 'pre') break; + blockIdx++; + } + if (blockIdx >= blocks.length) { + deferred.resolve(blocks); + return; + } + + var block = blocks[blockIdx]; + // 在 content-flow 中找到对应的 .content-flow-block + var $blockEl = $flow.find('.content-flow-block[data-index="' + blockIdx + '"]'); + if (!$blockEl.length) { + convertNextBlock(blockIdx + 1); + return; + } + + // 对代码块:先应用 Prism 高亮 + if (block.type === 'pre' && typeof Prism !== 'undefined') { + $blockEl.find('pre > code:not([class*="language-"])').addClass('language-plaintext'); + $blockEl.find('pre code').each(function () { Prism.highlightElement(this); }); + } + + // html2canvas 截图 + var targetEl = $blockEl[0]; + html2canvas(targetEl, { + scale: 2, + useCORS: true, + backgroundColor: block.type === 'table' ? '#ffffff' : '#f5f5f5', + logging: false + }).then(function (canvas) { + var imgData = canvas.toDataURL('image/jpeg', 0.92); + // 替换 block 的 html 和 type + block.html = ''; + block.type = 'img'; + + // 更新中间栏中的显示 + $blockEl.html(block.html); + + convertNextBlock(blockIdx + 1); + }).catch(function () { + // 截图失败,保留原样 + convertNextBlock(blockIdx + 1); + }); + } + + convertNextBlock(0); + return deferred.promise(); + } + /** * DOM实测高度 - 从#content-flow中读取每个块的实际渲染高度 * @param {Array} blocks - parseHtmlToBlocks 返回的块数组