diff --git a/public/static/js/phone-image.js b/public/static/js/phone-image.js
index 4fc9b09..35a448e 100644
--- a/public/static/js/phone-image.js
+++ b/public/static/js/phone-image.js
@@ -39,6 +39,25 @@ var PhoneImageEngine = (function () {
// ===== 分页结果 =====
var pages = [];
+ // ===== 截图缓存 =====
+ var convertedBlockCache = {}; // key: simpleHash(block.html), value: { imgHtml: '...' }
+
+ /**
+ * 简单字符串哈希 (djb2 变体)
+ * @param {string} str
+ * @returns {string} 哈希字符串
+ */
+ function simpleHash(str) {
+ var hash = 0;
+ if (!str) return '0';
+ for (var i = 0; i < str.length; i++) {
+ var char = str.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash; // 转32位整数
+ }
+ return '' + hash;
+ }
+
/**
* 确保隐藏渲染区域存在
*/
@@ -101,10 +120,20 @@ var PhoneImageEngine = (function () {
/**
* 渲染排版预览 - 生成分页后的HTML
* 管线: preprocess -> parse -> renderContentFlow -> measure -> paginate -> generate -> renderThumbnails
+ * 带并发锁:防止多次 render 同时执行造成数据混乱
* @returns {jQuery Deferred} resolves with pages array
*/
function render() {
var deferred = $.Deferred();
+
+ // 防止并发渲染
+ if (render._locked) {
+ // 标记有待处理的渲染请求,当前渲染完成后会自动重试
+ render._pending = true;
+ return deferred.reject('rendering').promise();
+ }
+ render._locked = true;
+
pages = [];
var sizeConfig = config.sizes[config.size] || config.sizes.xiaohongshu;
var pageHeight = sizeConfig.height;
@@ -148,11 +177,21 @@ var PhoneImageEngine = (function () {
// 渲染缩略图(右侧)- 异步截图
renderThumbnails(sizeConfig).then(function () {
+ render._locked = false;
+ // 如果渲染期间有新的渲染请求排队,自动触发
+ if (render._pending) {
+ render._pending = false;
+ render();
+ }
deferred.resolve(pages);
}).catch(function (err) {
+ render._locked = false;
deferred.reject(err);
});
});
+ }).catch(function (err) {
+ render._locked = false;
+ deferred.reject(err);
});
});
@@ -970,29 +1009,53 @@ var PhoneImageEngine = (function () {
/**
* 在中间栏 #content-flow 中将表格和代码块截图转为图片
* 在 renderContentFlow(blocks) 之后、measureBlockHeights(blocks) 之前调用
+ * 使用 convertedBlockCache 缓存已转换的图片,仅对新增/变更的 block 执行截图
* @param {Array} blocks - parseHtmlToBlocks 返回的块数组
* @returns {jQuery Deferred} resolves with blocks array
*/
function convertFlowBlocksToImages(blocks) {
var deferred = $.Deferred();
var $flow = $('#content-flow');
+ var blocksToConvert = []; // 需要重新截图的 block 索引
- function convertNextBlock(blockIdx) {
- // 找到下一个需要转换的 block
- while (blockIdx < blocks.length) {
- if (blocks[blockIdx].type === 'table' || blocks[blockIdx].type === 'pre') break;
- blockIdx++;
+ // 第一遍:检查缓存,命中则直接复用
+ for (var i = 0; i < blocks.length; i++) {
+ if (blocks[i].type !== 'table' && blocks[i].type !== 'pre') continue;
+
+ var cacheKey = simpleHash(blocks[i].html);
+ if (convertedBlockCache[cacheKey]) {
+ // 命中缓存
+ blocks[i].html = convertedBlockCache[cacheKey].imgHtml;
+ blocks[i].type = 'img';
+ // 更新中间栏显示
+ var $cachedEl = $flow.find('.content-flow-block[data-index="' + i + '"]');
+ if ($cachedEl.length) $cachedEl.html(blocks[i].html);
+ } else {
+ blocksToConvert.push(i);
}
- if (blockIdx >= blocks.length) {
+ }
+
+ if (blocksToConvert.length === 0) {
+ deferred.resolve(blocks);
+ return deferred.promise();
+ }
+
+ // 第二遍:串行截图未缓存的 block
+ var convertIdx = 0;
+
+ function convertNext() {
+ if (convertIdx >= blocksToConvert.length) {
deferred.resolve(blocks);
return;
}
+ var blockIdx = blocksToConvert[convertIdx];
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);
+ convertIdx++;
+ convertNext();
return;
}
@@ -1002,30 +1065,34 @@ var PhoneImageEngine = (function () {
$blockEl.find('pre code').each(function () { Prism.highlightElement(this); });
}
- // html2canvas 截图
- var targetEl = $blockEl[0];
- html2canvas(targetEl, {
+ html2canvas($blockEl[0], {
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 = '';
+ var imgHtml = '
';
+
+ // 存入缓存(用截图前的原始HTML做key)
+ var originalHtml = block.html;
+ var cacheKey = simpleHash(originalHtml);
+ convertedBlockCache[cacheKey] = { imgHtml: imgHtml };
+
+ block.html = imgHtml;
block.type = 'img';
+ $blockEl.html(imgHtml);
- // 更新中间栏中的显示
- $blockEl.html(block.html);
-
- convertNextBlock(blockIdx + 1);
+ convertIdx++;
+ convertNext();
}).catch(function () {
// 截图失败,保留原样
- convertNextBlock(blockIdx + 1);
+ convertIdx++;
+ convertNext();
});
}
- convertNextBlock(0);
+ convertNext();
return deferred.promise();
}
@@ -1094,6 +1161,12 @@ var PhoneImageEngine = (function () {
* @param {number} blockIndex - 在哪个块之后插入 (对应 break-inserter 的 data-after-index)
*/
function insertPageBreak(blockIndex) {
+ // 如果正在渲染,延迟重试
+ if (render._locked) {
+ setTimeout(function () { insertPageBreak(blockIndex); }, 200);
+ return;
+ }
+
var cleanHtml = preprocessContent(postData.content_html);
var $temp = $('