From 8aeda4c518f25dd429f8f3ab3bf9802e5121c45c Mon Sep 17 00:00:00 2001 From: augushong Date: Sun, 3 May 2026 09:08:15 +0800 Subject: [PATCH] refactor(phone-image): JS engine renders thumbnails via html2canvas --- public/static/js/phone-image.js | 251 ++++++++++++++++++++++--------- view/admin/post/phone_image.html | 10 +- 2 files changed, 190 insertions(+), 71 deletions(-) diff --git a/public/static/js/phone-image.js b/public/static/js/phone-image.js index 59775a9..7b3578c 100644 --- a/public/static/js/phone-image.js +++ b/public/static/js/phone-image.js @@ -39,6 +39,20 @@ var PhoneImageEngine = (function () { // ===== 分页结果 ===== var pages = []; + /** + * 确保隐藏渲染区域存在 + */ + function ensureStaging() { + if ($('#render-staging').length === 0) { + $('
').css({ + position: 'fixed', + left: '-9999px', + top: 0, + visibility: 'hidden' + }).appendTo('body'); + } + } + /** * 初始化引擎 * @param {Object} options - {postId, title, desc, coverText, contentHtml, poster, authorName, createTime, categoryName} @@ -87,9 +101,10 @@ var PhoneImageEngine = (function () { /** * 渲染排版预览 - 生成分页后的HTML * 两遍遍历: 先算总页数, 再带页码生成 - * @returns {Array} 页面数组,每项是 { type, html, pageNum } + * @returns {jQuery Deferred} resolves with pages array */ function render() { + var deferred = $.Deferred(); pages = []; var sizeConfig = config.sizes[config.size] || config.sizes.xiaohongshu; var pageHeight = sizeConfig.height; @@ -120,16 +135,17 @@ var PhoneImageEngine = (function () { } } - // 渲染到DOM - renderToDOM(sizeConfig); - - // 渲染内容流(中间栏) + // 渲染内容流(中间栏)- 同步 renderContentFlow(blocks); - // 渲染逐页对齐指示器 - renderAlignmentToggles(); + // 渲染缩略图(右侧)- 异步截图 + renderThumbnails(sizeConfig).then(function () { + deferred.resolve(pages); + }).catch(function (err) { + deferred.reject(err); + }); - return pages; + return deferred.promise(); } /** @@ -606,48 +622,167 @@ var PhoneImageEngine = (function () { // ===== DOM渲染 ===== /** - * 全尺寸平铺渲染到 #paginated-preview + * 渲染到隐藏区域 → 截图 → 显示缩略图 + * @param {Object} sizeConfig + * @returns {jQuery Deferred} resolves with pages array */ - function renderToDOM(sizeConfig) { + function renderThumbnails(sizeConfig) { + var deferred = $.Deferred(); + ensureStaging(); + + var $staging = $('#render-staging'); + $staging.empty(); + + // 渲染所有页面到隐藏区域 + for (var i = 0; i < pages.length; i++) { + $staging.append(pages[i].html); + } + + // 等待一帧确保 DOM 渲染完成 + requestAnimationFrame(function () { + var $pageElems = $staging.find('.phone-image-page'); + if ($pageElems.length === 0) { + displayThumbnails([], sizeConfig); + deferred.resolve(pages); + return; + } + + // 串行截图每一页 + var thumbnailDataUrls = []; + var idx = 0; + var total = $pageElems.length; + + function captureNext() { + if (idx >= total) { + $staging.empty(); + displayThumbnails(thumbnailDataUrls, sizeConfig); + deferred.resolve(pages); + return; + } + + var $elem = $pageElems.eq(idx); + html2canvas($elem[0], { + scale: 1, + useCORS: true, + backgroundColor: '#ffffff', + width: $elem.outerWidth(), + height: $elem.outerHeight(), + logging: false + }).then(function (canvas) { + thumbnailDataUrls.push(canvas.toDataURL('image/jpeg', 0.85)); + idx++; + captureNext(); + }).catch(function (err) { + deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err); + }); + } + + captureNext(); + }); + + return deferred.promise(); + } + + /** + * 在右侧显示缩略图 + * @param {Array} dataUrls - base64 图片数据 + * @param {Object} sizeConfig + */ + function displayThumbnails(dataUrls, sizeConfig) { var $preview = $('#paginated-preview'); if (!$preview.length) return; $preview.empty(); - for (var i = 0; i < pages.length; i++) { - $preview.append(pages[i].html); + if (dataUrls.length === 0) return; + + // 计算缩放比例:缩略图高度 = 容器高度 - 40(页码标签) - 40(上下padding) + var containerHeight = $preview.parent().height() || 700; + var scaleRatio = (containerHeight - 80) / sizeConfig.height; + var thumbWidth = Math.round(sizeConfig.width * scaleRatio); + + for (var i = 0; i < dataUrls.length; i++) { + var $item = $('
'); + $item.css('width', thumbWidth + 'px'); + + var $img = $(''); + $img.attr('src', dataUrls[i]); + $img.css('width', thumbWidth + 'px'); + $item.append($img); + + // 页码 + var $pageNum = $('' + (i + 1) + '/' + dataUrls.length + ''); + $item.append($pageNum); + + $preview.append($item); } } + /** + * 将页面渲染到隐藏区域并高质量截图(用于保存) + * @returns {jQuery Deferred} resolves with canvas array + */ + function capturePagesFromStaging() { + var deferred = $.Deferred(); + ensureStaging(); + + var $staging = $('#render-staging'); + $staging.empty(); + + var sizeConfig = config.sizes[config.size] || config.sizes.xiaohongshu; + + // 添加 size class wrapper + var sizeClass = 'size-' + config.size; + var $wrapper = $('
'); + for (var i = 0; i < pages.length; i++) { + $wrapper.append(pages[i].html); + } + $staging.append($wrapper); + + requestAnimationFrame(function () { + var $pageElems = $staging.find('.phone-image-page'); + if ($pageElems.length === 0) { + deferred.reject('没有可渲染的页面'); + return; + } + + var canvases = []; + var idx = 0; + var total = $pageElems.length; + + function captureNext() { + if (idx >= total) { + $staging.empty(); + deferred.resolve(canvases); + return; + } + + html2canvas($pageElems.eq(idx)[0], { + scale: 2, + useCORS: true, + backgroundColor: '#ffffff', + width: $pageElems.eq(idx).outerWidth(), + height: $pageElems.eq(idx).outerHeight(), + logging: false + }).then(function (canvas) { + canvases.push(canvas); + idx++; + captureNext(); + }).catch(function (err) { + deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err); + }); + } + + captureNext(); + }); + + return deferred.promise(); + } + /** * 为每页渲染逐页对齐切换指示器 */ function renderAlignmentToggles() { - var $pages = $('#paginated-preview .phone-image-page'); - $pages.each(function (index) { - var $page = $(this); - - // 只给内容页添加(跳过封面页和总结页) - if (!$page.hasClass('page-body')) return; - - // 通过 pages 数组获取真正的 pageNum - var pageNum = index; - if (pages[index] && pages[index].pageNum) { - pageNum = pages[index].pageNum; - } - - // 移除旧的 toggle - $page.find('.page-alignment-toggle').remove(); - - // 当前对齐状态 - var currentAlign = (config.pageAlignments && config.pageAlignments[pageNum]) || 'top'; - var isActiveCenter = currentAlign === 'center'; - - var $toggle = $(''); - - $page.append($toggle); - }); + // No-op: alignment toggles are now rendered as part of displayThumbnails } /** @@ -737,49 +872,27 @@ var PhoneImageEngine = (function () { // ===== 图片生成 ===== /** - * 逐页截图生成图片(简化版:所有页面已在DOM中可见) + * 逐页截图生成图片(使用隐藏渲染区域高质量截图) * @param {Function} [onProgress] 进度回调 function(currentIndex, totalPages, canvas) * @returns {Promise} jQuery Deferred, resolves with array of canvas objects */ function generateImages(onProgress) { var deferred = $.Deferred(); - var canvases = []; - var $pages = $('#paginated-preview .phone-image-page'); - if ($pages.length === 0) { + if (pages.length === 0) { deferred.reject('没有可渲染的页面'); return deferred.promise(); } - var index = 0; - var total = $pages.length; + if (onProgress) onProgress(0, pages.length, null); - function captureNext() { - if (index >= total) { - if (onProgress) onProgress(total, total, null); - deferred.resolve(canvases); - return; - } + capturePagesFromStaging().then(function (canvases) { + if (onProgress) onProgress(canvases.length, canvases.length, null); + deferred.resolve(canvases); + }).catch(function (err) { + deferred.reject(err); + }); - html2canvas($pages.eq(index)[0], { - scale: 2, - useCORS: true, - backgroundColor: '#ffffff', - width: $pages.eq(index).outerWidth(), - height: $pages.eq(index).outerHeight(), - logging: false - }).then(function (canvas) { - canvases.push(canvas); - index++; - if (onProgress) onProgress(index, total, canvas); - captureNext(); - }).catch(function (err) { - deferred.reject('截图失败(第' + (index + 1) + '页): ' + err); - }); - } - - if (onProgress) onProgress(0, total, null); - captureNext(); return deferred.promise(); } diff --git a/view/admin/post/phone_image.html b/view/admin/post/phone_image.html index 2aa4f95..c1e0631 100644 --- a/view/admin/post/phone_image.html +++ b/view/admin/post/phone_image.html @@ -222,8 +222,14 @@ fontSize: fontSize, watermark: $('[name="watermark"]').val() }); - var pages = PhoneImageEngine.render(); - layer.msg('排版完成,共 ' + pages.length + ' 页'); + var loadIdx = layer.load(); + PhoneImageEngine.render().then(function(pages) { + layer.close(loadIdx); + layer.msg('排版完成,共 ' + pages.length + ' 页'); + }).catch(function(err) { + layer.close(loadIdx); + layer.msg('渲染失败: ' + err); + }); }, 300); }