From 3ea3a6dbe383a6036582df6b547b3bdfcaf51a72 Mon Sep 17 00:00:00 2001 From: augushong Date: Sat, 16 May 2026 00:35:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(typesetting):=20Wave=202=20-=20=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E6=B8=B2=E6=9F=93=E3=80=81=E8=A1=A8=E6=A0=BC=E5=AD=97?= =?UTF-8?q?=E5=8F=B7=E7=8B=AC=E7=AB=8B=E6=8E=A7=E5=88=B6=E3=80=81=E4=BD=9C?= =?UTF-8?q?=E8=80=85=E5=A3=B0=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 缩略图改为流式渲染,截图一页即显示一页 - 新增tableFontScale独立控制表格字号,含后端持久化 - 内容页顶部添加作者声明(文/作者名),空值隐藏 --- app/common/tools/PhoneImage.php | 7 ++ public/static/css/phone-image-templates.css | 15 ++- public/static/js/phone-image.js | 114 +++++++++++++------- view/admin/post/phone_image.html | 38 ++++++- 4 files changed, 130 insertions(+), 44 deletions(-) diff --git a/app/common/tools/PhoneImage.php b/app/common/tools/PhoneImage.php index 3735497..621f077 100644 --- a/app/common/tools/PhoneImage.php +++ b/app/common/tools/PhoneImage.php @@ -20,6 +20,7 @@ class PhoneImage implements PostOutputManagerInterface 'watermark' => ['type' => 'text', 'default' => ''], 'pageAlignments' => ['type' => 'json', 'default' => '{}'], 'fontScale' => ['type' => 'number', 'default' => 1, 'min' => 0.5, 'max' => 2.0], + 'tableFontScale' => ['type' => 'number', 'default' => 1, 'min' => 0.5, 'max' => 2.0], ]; } @@ -45,6 +46,12 @@ class PhoneImage implements PostOutputManagerInterface return false; } } + if (isset($config['tableFontScale'])) { + $scale = floatval($config['tableFontScale']); + if ($scale < 0.5 || $scale > 2.0) { + return false; + } + } // watermark 是文本类型,无需特殊验证 // pageAlignments 是 JSON 类型,存储时由框架自动序列化 diff --git a/public/static/css/phone-image-templates.css b/public/static/css/phone-image-templates.css index 249016e..0d93485 100644 --- a/public/static/css/phone-image-templates.css +++ b/public/static/css/phone-image-templates.css @@ -50,6 +50,7 @@ --pi-radius: 12px; /* 字体 */ --pi-font-family: 'Source Han Sans', 'SourceHanSans-Normal', sans-serif; + --pi-table-font-scale: 1; } /* ============================================ @@ -251,6 +252,16 @@ margin-top: var(--pi-spacing-sm); } +/* 内容页 - 作者声明 */ +.author-credit { + font-size: 11px; + color: #999; + padding-bottom: 8px; + margin-bottom: 12px; + border-bottom: 1px solid #eee; + text-indent: 0; +} + /* 内容页 - 正文区域 */ .page-content { flex: 1; @@ -600,7 +611,7 @@ ============================================ */ /* 表格美化样式(html2canvas兼容) */ -.page-content table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: calc(13px * var(--pi-font-scale, 1)); } +.page-content table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: calc(13px * var(--pi-table-font-scale, 1)); } .page-content th { background-color: #f0f0f0; font-weight: bold; padding: 4px 10px; border: 1px solid #ddd; text-align: left; } .page-content td { padding: 4px 10px; border: 1px solid #ddd; } .page-content tr:nth-child(even) td { background-color: #f9f9f9; } @@ -628,7 +639,7 @@ } /* 中间栏表格样式 */ -.content-flow-block table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: calc(13px * var(--pi-font-scale, 1)); } +.content-flow-block table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: calc(13px * var(--pi-table-font-scale, 1)); } .content-flow-block th { background-color: #f0f0f0; font-weight: bold; padding: 4px 10px; border: 1px solid #ddd; text-align: left; } .content-flow-block td { padding: 4px 10px; border: 1px solid #ddd; } .content-flow-block tr:nth-child(even) td { background-color: #f9f9f9; } diff --git a/public/static/js/phone-image.js b/public/static/js/phone-image.js index 514b149..3cec47e 100644 --- a/public/static/js/phone-image.js +++ b/public/static/js/phone-image.js @@ -99,7 +99,8 @@ var PhoneImageEngine = (function () { douyin: { width: 540, height: 960 } }, contentPadding: 20, - fontScale: 1 + fontScale: 1, + tableFontScale: 1 }; // ===== 文章数据 ===== @@ -159,10 +160,17 @@ var PhoneImageEngine = (function () { */ function applyFontScale() { var scale = config.fontScale || 1; + var tableScale = config.tableFontScale || 1; var preview = document.getElementById('render-preview'); - if (preview) preview.style.setProperty('--pi-font-scale', scale); + if (preview) { + preview.style.setProperty('--pi-font-scale', scale); + preview.style.setProperty('--pi-table-font-scale', tableScale); + } var staging = document.getElementById('render-staging'); - if (staging) staging.style.setProperty('--pi-font-scale', scale); + if (staging) { + staging.style.setProperty('--pi-font-scale', scale); + staging.style.setProperty('--pi-table-font-scale', tableScale); + } } /** @@ -914,6 +922,11 @@ var PhoneImageEngine = (function () { html += ''; } + // 作者声明(所有内容页) + if (postData.author_name) { + html += '
文/' + escapeHtml(postData.author_name) + '
'; + } + // 正文内容区 html += '
'; for (var i = 0; i < blocks.length; i++) { @@ -1125,17 +1138,19 @@ var PhoneImageEngine = (function () { // 保存模式: 高质量绘制原图到canvas renderPureImageToCanvas(pureSrc, opts.sizeConfig.width, opts.sizeConfig.height).then(function (canvas) { results.push(canvas); + if (opts.streaming && opts.onPageReady) opts.onPageReady(canvas, idx); if (pageProgressCallback) pageProgressCallback(idx + 1, total); idx++; captureNext(); }).catch(function () { // 加载失败,回退到html2canvas - capturePageViaHtml2Canvas($elem, $staging, opts, results, deferred, function () { if (pageProgressCallback) pageProgressCallback(idx + 1, total); idx++; captureNext(); }); + capturePageViaHtml2Canvas($elem, $staging, opts, results, deferred, function () { if (opts.streaming && opts.onPageReady) opts.onPageReady(results[results.length - 1], idx); if (pageProgressCallback) pageProgressCallback(idx + 1, total); idx++; captureNext(); }); }); return; } else { // 缩略图模式: 直接用src results.push(pureSrc); + if (opts.streaming && opts.onPageReady) opts.onPageReady(pureSrc, idx); if (pageProgressCallback) pageProgressCallback(idx + 1, total); idx++; captureNext(); @@ -1144,7 +1159,7 @@ var PhoneImageEngine = (function () { } } - capturePageViaHtml2Canvas($elem, $staging, opts, results, deferred, function () { idx++; captureNext(); }); + capturePageViaHtml2Canvas($elem, $staging, opts, results, deferred, function () { if (opts.streaming && opts.onPageReady) opts.onPageReady(results[results.length - 1], idx); idx++; captureNext(); }); } captureNext(); @@ -1183,15 +1198,22 @@ var PhoneImageEngine = (function () { function renderThumbnails(sizeConfig) { var deferred = $.Deferred(); + // 流式渲染:先清空容器,再逐页追加缩略图 + initThumbnails(sizeConfig); + doCapturePages({ scale: 1, outputCanvas: false, quality: 0.85, - sizeConfig: sizeConfig + sizeConfig: sizeConfig, + streaming: true, + onPageReady: function (dataUrl, pageIndex) { + appendThumbnail(dataUrl, pageIndex, sizeConfig, pages[pageIndex]); + } }, function (current, total) { PhoneImageLogPanel.log('截图: 第 ' + current + '/' + total + ' 页', 'info'); }).then(function (dataUrls) { - displayThumbnails(dataUrls, sizeConfig); + updateThumbnailPageNumbers(dataUrls.length); deferred.resolve(pages); }).catch(function (err) { deferred.reject(err); @@ -1201,51 +1223,69 @@ var PhoneImageEngine = (function () { } /** - * 在右侧显示缩略图 - * @param {Array} dataUrls - base64 图片数据 + * 初始化缩略图容器(流式渲染第一步:清空容器) * @param {Object} sizeConfig */ - function displayThumbnails(dataUrls, sizeConfig) { + function initThumbnails(sizeConfig) { var $preview = $('#paginated-preview'); if (!$preview.length) return; - $preview.empty(); - if (dataUrls.length === 0) return; + } + + /** + * 追加单个缩略图到预览区(流式渲染:每页截图完成后立即追加) + * @param {string} dataUrl - base64 图片数据或图片src + * @param {number} pageIndex - 页面索引(0-based) + * @param {Object} sizeConfig + * @param {Object} pageData - 页面对象 { type, pageNum, ... } + */ + function appendThumbnail(dataUrl, pageIndex, sizeConfig, pageData) { + var $preview = $('#paginated-preview'); + if (!$preview.length) 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 $item = $('
'); + $item.css('width', thumbWidth + 'px'); - var $img = $(''); - $img.attr('src', dataUrls[i]); - $img.css('width', thumbWidth + 'px'); - $item.append($img); + var $img = $(''); + $img.attr('src', dataUrl); + $img.css('width', thumbWidth + 'px'); + $item.append($img); - // 页码 - var $pageNum = $('' + (i + 1) + '/' + dataUrls.length + ''); - $item.append($pageNum); + // 页码(初始只显示序号,全部完成后更新为 N/M) + var $pageNum = $('' + (pageIndex + 1) + ''); + $item.append($pageNum); - // 对齐按钮(仅内容页) - if (pages[i] && pages[i].type === 'content') { - var pageNum = pages[i].pageNum || (i); - var currentAlign = (config.pageAlignments && config.pageAlignments[pageNum]) || 'top'; - var isActiveCenter = currentAlign === 'center'; - var isActiveBottom = currentAlign === 'bottom'; - var activeClass = isActiveCenter ? ' active-center' : (isActiveBottom ? ' active-bottom' : ''); - var symbol = isActiveCenter ? '\u2195' : (isActiveBottom ? '\u2193' : '\u2191'); - var $toggle = $(''); - $item.append($toggle); - } - - $preview.append($item); + // 对齐按钮(仅内容页) + if (pageData && pageData.type === 'content') { + var pageNum = pageData.pageNum || (pageIndex); + var currentAlign = (config.pageAlignments && config.pageAlignments[pageNum]) || 'top'; + var isActiveCenter = currentAlign === 'center'; + var isActiveBottom = currentAlign === 'bottom'; + var activeClass = isActiveCenter ? ' active-center' : (isActiveBottom ? ' active-bottom' : ''); + var symbol = isActiveCenter ? '\u2195' : (isActiveBottom ? '\u2193' : '\u2191'); + var $toggle = $(''); + $item.append($toggle); } + + $preview.append($item); + } + + /** + * 更新所有缩略图页码为 N/M 格式(流式渲染全部完成后调用) + * @param {number} totalPages + */ + function updateThumbnailPageNumbers(totalPages) { + var $items = $('#paginated-preview .preview-thumb-item'); + $items.each(function (i) { + $(this).find('.preview-thumb-page-num').text((i + 1) + '/' + totalPages); + }); } /** diff --git a/view/admin/post/phone_image.html b/view/admin/post/phone_image.html index 5af8887..976aeb9 100644 --- a/view/admin/post/phone_image.html +++ b/view/admin/post/phone_image.html @@ -285,7 +285,8 @@ var currentConfig = { size: 'xiaohongshu', watermark: '', - fontScale: 1 + fontScale: 1, + tableFontScale: 1 }; var postData = { @@ -306,13 +307,15 @@ size: savedConfig.size || 'xiaohongshu', watermark: savedConfig.watermark || '', pageAlignments: savedConfig.pageAlignments || {}, - fontScale: savedConfig.fontScale || 1 + fontScale: savedConfig.fontScale || 1, + tableFontScale: savedConfig.tableFontScale || 1 }; // 同步当前配置 currentConfig.size = initConfig.size; currentConfig.watermark = initConfig.watermark; currentConfig.fontScale = initConfig.fontScale; + currentConfig.tableFontScale = initConfig.tableFontScale; // ========== wangeditor 初始化 ========== var E = window.wangEditor; @@ -467,6 +470,7 @@ size: currentConfig.size, watermark: currentConfig.watermark, fontScale: currentConfig.fontScale || 1, + tableFontScale: currentConfig.tableFontScale || 1, content_html: PhoneImageEngine.getContentHtml() }, saveConfigUrl).then(function(data) { if (data.output_id) lastOutputId = data.output_id; @@ -533,13 +537,27 @@ settingsHtml += ''; settingsHtml += '
'; settingsHtml += '
'; + settingsHtml += '
'; + settingsHtml += ''; + settingsHtml += '
'; + settingsHtml += ''; + settingsHtml += '
'; + settingsHtml += '
'; settingsHtml += ''; settingsHtml += '
'; layer.open({ type: 1, title: '排版设置', - area: ['400px', '220px'], + area: ['400px', '280px'], content: settingsHtml, btn: ['确定', '取消'], success: function (layero) { @@ -548,8 +566,10 @@ yes: function (index, layero) { currentConfig.size = layero.find('[name="s_size"]').val(); currentConfig.watermark = layero.find('[name="s_watermark"]').val(); + var tblScaleSel = layero.find('[name="s_table_font_scale"]').val(); + currentConfig.tableFontScale = tblScaleSel === 'custom' ? (currentConfig.tableFontScale || 1) : parseFloat(tblScaleSel); layer.close(index); - PhoneImageLogPanel.log('应用新设置: 尺寸=' + currentConfig.size, 'info'); + PhoneImageLogPanel.log('应用新设置: 尺寸=' + currentConfig.size + ', 表格字号=' + (currentConfig.tableFontScale || 1) + 'x', 'info'); doRender(); } }); @@ -562,7 +582,8 @@ renderTimer = setTimeout(function () { var newConfig = { size: currentConfig.size, - watermark: currentConfig.watermark + watermark: currentConfig.watermark, + tableFontScale: currentConfig.tableFontScale || 1 }; if (extraConfig) { $.extend(newConfig, extraConfig); @@ -634,6 +655,7 @@ size: currentConfig.size, watermark: currentConfig.watermark, fontScale: currentConfig.fontScale || 1, + tableFontScale: currentConfig.tableFontScale || 1, content_html: PhoneImageEngine.getContentHtml() }, saveConfigUrl).then(function (data) { if (data.output_id) lastOutputId = data.output_id; @@ -662,6 +684,7 @@ size: currentConfig.size, watermark: currentConfig.watermark, fontScale: currentConfig.fontScale || 1, + tableFontScale: currentConfig.tableFontScale || 1, content_html: PhoneImageEngine.getContentHtml() }, saveConfigUrl).then(function (data) { if (data.output_id) lastOutputId = data.output_id; @@ -688,6 +711,7 @@ size: currentConfig.size, watermark: currentConfig.watermark, fontScale: currentConfig.fontScale || 1, + tableFontScale: currentConfig.tableFontScale || 1, content_html: PhoneImageEngine.getContentHtml() }, function(current, total, canvas) { if (total > 0) { @@ -779,6 +803,7 @@ if (cfg.watermark !== undefined) historyConfig.watermark = cfg.watermark; if (cfg.pageAlignments) historyConfig.pageAlignments = cfg.pageAlignments; if (cfg.fontScale !== undefined) historyConfig.fontScale = cfg.fontScale; + if (cfg.tableFontScale !== undefined) historyConfig.tableFontScale = cfg.tableFontScale; // 同步当前配置 if (cfg.size) currentConfig.size = cfg.size; @@ -787,6 +812,9 @@ currentConfig.fontScale = cfg.fontScale; setFontScaleUI(cfg.fontScale); } + if (cfg.tableFontScale !== undefined) { + currentConfig.tableFontScale = cfg.tableFontScale; + } lastOutputId = outputId; PhoneImageLogPanel.log('历史配置已加载', 'success');