feat(phone-image): 添加字号倍数控制功能

- 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 持久化
This commit is contained in:
augushong
2026-05-15 00:50:57 +08:00
parent 8ad90a28c0
commit 5a81385448
4 changed files with 104 additions and 38 deletions

View File

@@ -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 类型,存储时由框架自动序列化

View File

@@ -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 {

View File

@@ -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();
}
};
})();

View File

@@ -165,7 +165,15 @@
<!-- 中间:渲染预览区 -->
<div class="render-preview-area">
<div class="preview-header">渲染预览</div>
<div class="preview-header">
<span>渲染预览</span>
<div style="display:inline-flex;align-items:center;gap:8px;float:right;">
<span style="font-size:12px;color:#666;">字号</span>
<input type="range" id="font-scale-slider" min="0.5" max="2.0" step="0.1" value="1.0"
style="width:100px;vertical-align:middle;">
<span id="font-scale-value" style="font-size:12px;color:#1890ff;min-width:32px;">1.0x</span>
</div>
</div>
<div id="render-preview"></div>
</div>
@@ -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 = '<div class="settings-form" style="padding:20px 20px 0;">';
@@ -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();