From 5a81385448a9f04073de4d26d02a7353ea6a60a6 Mon Sep 17 00:00:00 2001 From: augushong Date: Fri, 15 May 2026 00:50:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(phone-image):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AD=97=E5=8F=B7=E5=80=8D=E6=95=B0=E6=8E=A7=E5=88=B6=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CSS: 新增 --pi-font-scale 变量,全量 font-size 支持 calc 缩放 - JS: config.fontScale 影响分页计算,applyFontScale() 同步CSS变量 - HTML: 渲染预览区 Slider 控件(0.5x~2.0x),拖动即时预览,松手完整渲染 - 后端: PhoneImage.php 新增 fontScale 配置字段和校验 - 所有保存路径(autoSave/save/generate)包含 fontScale 持久化 --- app/common/tools/PhoneImage.php | 7 +++ public/static/css/phone-image-templates.css | 68 +++++++++++---------- public/static/js/phone-image.js | 16 ++++- view/admin/post/phone_image.html | 51 +++++++++++++++- 4 files changed, 104 insertions(+), 38 deletions(-) diff --git a/app/common/tools/PhoneImage.php b/app/common/tools/PhoneImage.php index 0988831..3735497 100644 --- a/app/common/tools/PhoneImage.php +++ b/app/common/tools/PhoneImage.php @@ -19,6 +19,7 @@ class PhoneImage implements PostOutputManagerInterface 'fontSize' => ['type' => 'number', 'default' => 14, 'min' => 10, 'max' => 24], 'watermark' => ['type' => 'text', 'default' => ''], 'pageAlignments' => ['type' => 'json', 'default' => '{}'], + 'fontScale' => ['type' => 'number', 'default' => 1, 'min' => 0.5, 'max' => 2.0], ]; } @@ -38,6 +39,12 @@ class PhoneImage implements PostOutputManagerInterface return false; } } + if (isset($config['fontScale'])) { + $scale = floatval($config['fontScale']); + 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 4481255..d4535da 100644 --- a/public/static/css/phone-image-templates.css +++ b/public/static/css/phone-image-templates.css @@ -11,11 +11,13 @@ /* --- CSS Custom Properties (小红书经典风格) --- */ :root { + /* 字号缩放 */ + --pi-font-scale: 1; /* 字号 */ - --pi-font-size-base: 14px; - --pi-font-size-title: 26px; - --pi-font-size-subtitle: 16px; - --pi-font-size-small: 12px; + --pi-font-size-base: calc(14px * var(--pi-font-scale, 1)); + --pi-font-size-title: calc(26px * var(--pi-font-scale, 1)); + --pi-font-size-subtitle: calc(16px * var(--pi-font-scale, 1)); + --pi-font-size-small: calc(12px * var(--pi-font-scale, 1)); /* 行高 */ --pi-line-height: 1.8; /* 间距 */ @@ -66,7 +68,7 @@ position: absolute; bottom: 60px; right: 20px; - font-size: 12px; + font-size: calc(12px * var(--pi-font-scale, 1)); color: rgba(0, 0, 0, 0.3); pointer-events: none; white-space: nowrap; @@ -105,7 +107,7 @@ } .page-cover .cover-title { - font-size: 28px; + font-size: calc(28px * var(--pi-font-scale, 1)); font-weight: normal; letter-spacing: 2px; line-height: 1.5; @@ -163,7 +165,7 @@ } .page-cover.no-cover-image .cover-no-img-title { - font-size: 36px; + font-size: calc(36px * var(--pi-font-scale, 1)); font-weight: bold; line-height: 1.4; color: var(--pi-color-text); @@ -220,7 +222,7 @@ } .page-header .page-title { - font-size: 20px; + font-size: calc(20px * var(--pi-font-scale, 1)); font-weight: normal; letter-spacing: 1px; color: #666; @@ -250,7 +252,7 @@ .page-content h2 { font-weight: normal; - font-size: 18px; + font-size: calc(18px * var(--pi-font-scale, 1)); letter-spacing: 1px; margin-top: 24px; margin-bottom: 12px; @@ -259,28 +261,28 @@ .page-content h3 { font-weight: normal; - font-size: 16px; + font-size: calc(16px * var(--pi-font-scale, 1)); margin-top: 20px; margin-bottom: 10px; color: #555; } .page-content h4 { - font-size: 14px; + font-size: calc(14px * var(--pi-font-scale, 1)); font-weight: bold; margin-top: var(--pi-spacing-sm); margin-bottom: 5px; } .page-content h5 { - font-size: 13px; + font-size: calc(13px * var(--pi-font-scale, 1)); font-weight: bold; margin-top: var(--pi-spacing-sm); margin-bottom: 5px; } .page-content h6 { - font-size: 12px; + font-size: calc(12px * var(--pi-font-scale, 1)); font-weight: bold; margin-top: var(--pi-spacing-sm); margin-bottom: 5px; @@ -310,7 +312,7 @@ border-top: none; padding-top: 0; margin-top: var(--pi-spacing-sm); - font-size: 11px; + font-size: calc(11px * var(--pi-font-scale, 1)); color: #bbb; display: flex; justify-content: space-between; @@ -330,7 +332,7 @@ .page-summary .summary-title { font-weight: normal; letter-spacing: 2px; - font-size: 22px; + font-size: calc(22px * var(--pi-font-scale, 1)); margin-bottom: var(--pi-spacing); color: var(--pi-color-text); } @@ -447,7 +449,7 @@ border-radius: 50%; background: var(--pi-color-accent); color: #fff; - font-size: 14px; + font-size: calc(14px * var(--pi-font-scale, 1)); line-height: 20px; text-align: center; border: none; @@ -473,7 +475,7 @@ } .page-break-marker .break-marker-label { - font-size: 12px; + font-size: calc(12px * var(--pi-font-scale, 1)); color: #ff4d4f; user-select: none; } @@ -488,7 +490,7 @@ border-radius: 50%; background: #ff4d4f; color: #fff; - font-size: 14px; + font-size: calc(14px * var(--pi-font-scale, 1)); line-height: 20px; text-align: center; border: none; @@ -529,7 +531,7 @@ .preview-thumb-page-num { display: block; text-align: center; - font-size: 12px; + font-size: calc(12px * var(--pi-font-scale, 1)); color: #999; margin-top: 6px; } @@ -547,7 +549,7 @@ display: flex; align-items: center; justify-content: center; - font-size: 14px; + font-size: calc(14px * var(--pi-font-scale, 1)); color: #666; user-select: none; border: none; @@ -576,7 +578,7 @@ ============================================ */ /* 表格美化样式(html2canvas兼容) */ -.page-content table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 13px; } +.page-content table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: calc(13px * var(--pi-font-scale, 1)); } .page-content th { background-color: #f0f0f0; font-weight: bold; padding: 8px 10px; border: 1px solid #ddd; text-align: left; } .page-content td { padding: 8px 10px; border: 1px solid #ddd; } .page-content tr:nth-child(even) td { background-color: #f9f9f9; } @@ -587,14 +589,14 @@ border-radius: 4px; padding: 12px; overflow-x: auto; - font-size: 13px; + font-size: calc(13px * var(--pi-font-scale, 1)); line-height: 1.6; margin: 10px 0; } .content-flow code { font-family: 'Courier New', Consolas, monospace; - font-size: 13px; + font-size: calc(13px * var(--pi-font-scale, 1)); } .content-flow pre code { @@ -604,7 +606,7 @@ } /* 中间栏表格样式 */ -.content-flow-block table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 13px; } +.content-flow-block table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: calc(13px * var(--pi-font-scale, 1)); } .content-flow-block th { background-color: #f0f0f0; font-weight: bold; padding: 8px 10px; border: 1px solid #ddd; text-align: left; } .content-flow-block td { padding: 8px 10px; border: 1px solid #ddd; } .content-flow-block tr:nth-child(even) td { background-color: #f9f9f9; } @@ -785,7 +787,7 @@ body > .page-header-right .layui-btn:not(.layui-btn-primary):not(.layui-btn-norm /* 预览区标题字号 */ #render-preview h1 { - font-size: 22px; + font-size: calc(22px * var(--pi-font-scale, 1)); font-weight: bold; letter-spacing: 1px; margin-top: 24px; @@ -795,7 +797,7 @@ body > .page-header-right .layui-btn:not(.layui-btn-primary):not(.layui-btn-norm #render-preview h2 { font-weight: normal; - font-size: 18px; + font-size: calc(18px * var(--pi-font-scale, 1)); letter-spacing: 1px; margin-top: 24px; margin-bottom: 12px; @@ -803,27 +805,27 @@ body > .page-header-right .layui-btn:not(.layui-btn-primary):not(.layui-btn-norm #render-preview h3 { font-weight: normal; - font-size: 16px; + font-size: calc(16px * var(--pi-font-scale, 1)); margin-top: 20px; margin-bottom: 10px; } #render-preview h4 { - font-size: 14px; + font-size: calc(14px * var(--pi-font-scale, 1)); font-weight: bold; margin-top: 10px; margin-bottom: 5px; } #render-preview h5 { - font-size: 13px; + font-size: calc(13px * var(--pi-font-scale, 1)); font-weight: bold; margin-top: 10px; margin-bottom: 5px; } #render-preview h6 { - font-size: 12px; + font-size: calc(12px * var(--pi-font-scale, 1)); font-weight: bold; margin-top: 10px; margin-bottom: 5px; @@ -850,7 +852,7 @@ body > .page-header-right .layui-btn:not(.layui-btn-primary):not(.layui-btn-norm width: 100%; border-collapse: collapse; margin: 10px 0; - font-size: 13px; + font-size: calc(13px * var(--pi-font-scale, 1)); } #render-preview th { @@ -877,14 +879,14 @@ body > .page-header-right .layui-btn:not(.layui-btn-primary):not(.layui-btn-norm border-radius: 4px; padding: 12px; overflow-x: auto; - font-size: 13px; + font-size: calc(13px * var(--pi-font-scale, 1)); line-height: 1.6; margin: 10px 0; } #render-preview code { font-family: 'Courier New', Consolas, monospace; - font-size: 13px; + font-size: calc(13px * var(--pi-font-scale, 1)); } #render-preview pre code { diff --git a/public/static/js/phone-image.js b/public/static/js/phone-image.js index 019a41a..5a78546 100644 --- a/public/static/js/phone-image.js +++ b/public/static/js/phone-image.js @@ -19,7 +19,8 @@ var PhoneImageEngine = (function () { xiaohongshu: { width: 540, height: 720 }, douyin: { width: 540, height: 960 } }, - contentPadding: 20 + contentPadding: 20, + fontScale: 1 }; // ===== 文章数据 ===== @@ -71,6 +72,15 @@ var PhoneImageEngine = (function () { if (userConfig) { $.extend(config, userConfig); } + applyFontScale(); + } + + /** + * 应用字号缩放到CSS变量 + */ + function applyFontScale() { + var scale = config.fontScale || 1; + document.documentElement.style.setProperty('--pi-font-scale', scale); } /** @@ -656,7 +666,7 @@ var PhoneImageEngine = (function () { } // 按句子拆分:句号、问号、叹号、换行(兼容不支持 lookbehind 的浏览器) - var effectiveFontSize = 14; + var effectiveFontSize = 14 * (config.fontScale || 1); var parts = text.split(/([。!?\n])/); var sentences = []; var current = ''; @@ -1269,6 +1279,7 @@ var PhoneImageEngine = (function () { config: { size: saveConfig.size || config.size, watermark: saveConfig.watermark || config.watermark, + fontScale: config.fontScale || 1, pageAlignments: config.pageAlignments || {} }, content_html: saveConfig.content_html || postData.content_html @@ -1488,6 +1499,7 @@ var PhoneImageEngine = (function () { if (newConfig) { $.extend(config, newConfig); } + applyFontScale(); } }; })(); diff --git a/view/admin/post/phone_image.html b/view/admin/post/phone_image.html index ad298bb..4f8b080 100644 --- a/view/admin/post/phone_image.html +++ b/view/admin/post/phone_image.html @@ -165,7 +165,15 @@
-
渲染预览
+
+ 渲染预览 +
+ 字号 + + 1.0x +
+
@@ -195,7 +203,8 @@ // 当前排版配置(从设置弹框维护) var currentConfig = { size: 'xiaohongshu', - watermark: '' + watermark: '', + fontScale: 1 }; var postData = { @@ -215,12 +224,14 @@ var initConfig = { size: savedConfig.size || 'xiaohongshu', watermark: savedConfig.watermark || '', - pageAlignments: savedConfig.pageAlignments || {} + pageAlignments: savedConfig.pageAlignments || {}, + fontScale: savedConfig.fontScale || 1 }; // 同步当前配置 currentConfig.size = initConfig.size; currentConfig.watermark = initConfig.watermark; + currentConfig.fontScale = initConfig.fontScale; // ========== wangeditor 初始化 ========== var E = window.wangEditor; @@ -331,6 +342,31 @@ // 初始同步预览区 PhoneImageEngine.syncPreview(); + // 字号倍数 Slider + var $slider = $('#font-scale-slider'); + var $scaleValue = $('#font-scale-value'); + + // 拖动中: 仅更新CSS变量和显示值,不触发html2canvas + $slider.on('input', function() { + var val = parseFloat($(this).val()); + $scaleValue.text(val.toFixed(1) + 'x'); + currentConfig.fontScale = val; + document.documentElement.style.setProperty('--pi-font-scale', val); + PhoneImageEngine.updateConfig({ fontScale: val }); + }); + + // 松手后: 触发完整渲染(重新分页 + html2canvas截图) + $slider.on('change', function() { + var val = parseFloat($(this).val()); + doRender({ fontScale: val }); + }); + + // 初始化时更新 Slider 显示值 + if (initConfig.fontScale && initConfig.fontScale !== 1) { + $slider.val(initConfig.fontScale); + $scaleValue.text(parseFloat(initConfig.fontScale).toFixed(1) + 'x'); + } + // ========== 设置弹框 ========== $('#btn-settings').click(function () { var settingsHtml = '
'; @@ -451,6 +487,7 @@ PhoneImageEngine.saveConfig(postData.postId, { size: currentConfig.size, watermark: currentConfig.watermark, + fontScale: currentConfig.fontScale || 1, content_html: PhoneImageEngine.getContentHtml() }, saveConfigUrl).then(function (data) { if (data.output_id) lastOutputId = data.output_id; @@ -476,6 +513,7 @@ PhoneImageEngine.saveConfig(postData.postId, { size: currentConfig.size, watermark: currentConfig.watermark, + fontScale: currentConfig.fontScale || 1, content_html: PhoneImageEngine.getContentHtml() }, saveConfigUrl).then(function (data) { if (data.output_id) lastOutputId = data.output_id; @@ -499,6 +537,7 @@ PhoneImageEngine.saveImages(postData.postId, { size: currentConfig.size, watermark: currentConfig.watermark, + fontScale: currentConfig.fontScale || 1, content_html: PhoneImageEngine.getContentHtml() }).then(function (data) { if (data.output_id) { @@ -584,10 +623,16 @@ if (cfg.size) historyConfig.size = cfg.size; 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.size) currentConfig.size = cfg.size; if (cfg.watermark !== undefined) currentConfig.watermark = cfg.watermark; + if (cfg.fontScale !== undefined) { + currentConfig.fontScale = cfg.fontScale; + $slider.val(cfg.fontScale); + $scaleValue.text(parseFloat(cfg.fontScale).toFixed(1) + 'x'); + } lastOutputId = outputId; layer.closeAll();