mirror of
https://gitee.com/ulthon/ulthon_information.git
synced 2026-07-10 06:22:49 +08:00
refactor(phone-image): JS engine renders thumbnails via html2canvas
This commit is contained in:
@@ -39,6 +39,20 @@ var PhoneImageEngine = (function () {
|
|||||||
// ===== 分页结果 =====
|
// ===== 分页结果 =====
|
||||||
var pages = [];
|
var pages = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保隐藏渲染区域存在
|
||||||
|
*/
|
||||||
|
function ensureStaging() {
|
||||||
|
if ($('#render-staging').length === 0) {
|
||||||
|
$('<div id="render-staging">').css({
|
||||||
|
position: 'fixed',
|
||||||
|
left: '-9999px',
|
||||||
|
top: 0,
|
||||||
|
visibility: 'hidden'
|
||||||
|
}).appendTo('body');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化引擎
|
* 初始化引擎
|
||||||
* @param {Object} options - {postId, title, desc, coverText, contentHtml, poster, authorName, createTime, categoryName}
|
* @param {Object} options - {postId, title, desc, coverText, contentHtml, poster, authorName, createTime, categoryName}
|
||||||
@@ -87,9 +101,10 @@ var PhoneImageEngine = (function () {
|
|||||||
/**
|
/**
|
||||||
* 渲染排版预览 - 生成分页后的HTML
|
* 渲染排版预览 - 生成分页后的HTML
|
||||||
* 两遍遍历: 先算总页数, 再带页码生成
|
* 两遍遍历: 先算总页数, 再带页码生成
|
||||||
* @returns {Array} 页面数组,每项是 { type, html, pageNum }
|
* @returns {jQuery Deferred} resolves with pages array
|
||||||
*/
|
*/
|
||||||
function render() {
|
function render() {
|
||||||
|
var deferred = $.Deferred();
|
||||||
pages = [];
|
pages = [];
|
||||||
var sizeConfig = config.sizes[config.size] || config.sizes.xiaohongshu;
|
var sizeConfig = config.sizes[config.size] || config.sizes.xiaohongshu;
|
||||||
var pageHeight = sizeConfig.height;
|
var pageHeight = sizeConfig.height;
|
||||||
@@ -120,16 +135,17 @@ var PhoneImageEngine = (function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染到DOM
|
// 渲染内容流(中间栏)- 同步
|
||||||
renderToDOM(sizeConfig);
|
|
||||||
|
|
||||||
// 渲染内容流(中间栏)
|
|
||||||
renderContentFlow(blocks);
|
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渲染 =====
|
// ===== 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');
|
var $preview = $('#paginated-preview');
|
||||||
if (!$preview.length) return;
|
if (!$preview.length) return;
|
||||||
|
|
||||||
$preview.empty();
|
$preview.empty();
|
||||||
for (var i = 0; i < pages.length; i++) {
|
if (dataUrls.length === 0) return;
|
||||||
$preview.append(pages[i].html);
|
|
||||||
|
// 计算缩放比例:缩略图高度 = 容器高度 - 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 = $('<div class="preview-thumb-item" data-page-index="' + i + '">');
|
||||||
|
$item.css('width', thumbWidth + 'px');
|
||||||
|
|
||||||
|
var $img = $('<img>');
|
||||||
|
$img.attr('src', dataUrls[i]);
|
||||||
|
$img.css('width', thumbWidth + 'px');
|
||||||
|
$item.append($img);
|
||||||
|
|
||||||
|
// 页码
|
||||||
|
var $pageNum = $('<span class="preview-thumb-page-num">' + (i + 1) + '/' + dataUrls.length + '</span>');
|
||||||
|
$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 = $('<div class="phone-image-container ' + sizeClass + '">');
|
||||||
|
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() {
|
function renderAlignmentToggles() {
|
||||||
var $pages = $('#paginated-preview .phone-image-page');
|
// No-op: alignment toggles are now rendered as part of displayThumbnails
|
||||||
$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 = $('<button class="page-alignment-toggle' + (isActiveCenter ? ' active-center' : '') + '" data-page-num="' + pageNum + '">' +
|
|
||||||
(isActiveCenter ? '\u2195' : '\u2191') +
|
|
||||||
'</button>');
|
|
||||||
|
|
||||||
$page.append($toggle);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -737,49 +872,27 @@ var PhoneImageEngine = (function () {
|
|||||||
// ===== 图片生成 =====
|
// ===== 图片生成 =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 逐页截图生成图片(简化版:所有页面已在DOM中可见)
|
* 逐页截图生成图片(使用隐藏渲染区域高质量截图)
|
||||||
* @param {Function} [onProgress] 进度回调 function(currentIndex, totalPages, canvas)
|
* @param {Function} [onProgress] 进度回调 function(currentIndex, totalPages, canvas)
|
||||||
* @returns {Promise} jQuery Deferred, resolves with array of canvas objects
|
* @returns {Promise} jQuery Deferred, resolves with array of canvas objects
|
||||||
*/
|
*/
|
||||||
function generateImages(onProgress) {
|
function generateImages(onProgress) {
|
||||||
var deferred = $.Deferred();
|
var deferred = $.Deferred();
|
||||||
var canvases = [];
|
|
||||||
var $pages = $('#paginated-preview .phone-image-page');
|
|
||||||
|
|
||||||
if ($pages.length === 0) {
|
if (pages.length === 0) {
|
||||||
deferred.reject('没有可渲染的页面');
|
deferred.reject('没有可渲染的页面');
|
||||||
return deferred.promise();
|
return deferred.promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
var index = 0;
|
if (onProgress) onProgress(0, pages.length, null);
|
||||||
var total = $pages.length;
|
|
||||||
|
|
||||||
function captureNext() {
|
capturePagesFromStaging().then(function (canvases) {
|
||||||
if (index >= total) {
|
if (onProgress) onProgress(canvases.length, canvases.length, null);
|
||||||
if (onProgress) onProgress(total, total, null);
|
deferred.resolve(canvases);
|
||||||
deferred.resolve(canvases);
|
}).catch(function (err) {
|
||||||
return;
|
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();
|
return deferred.promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -222,8 +222,14 @@
|
|||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
watermark: $('[name="watermark"]').val()
|
watermark: $('[name="watermark"]').val()
|
||||||
});
|
});
|
||||||
var pages = PhoneImageEngine.render();
|
var loadIdx = layer.load();
|
||||||
layer.msg('排版完成,共 ' + pages.length + ' 页');
|
PhoneImageEngine.render().then(function(pages) {
|
||||||
|
layer.close(loadIdx);
|
||||||
|
layer.msg('排版完成,共 ' + pages.length + ' 页');
|
||||||
|
}).catch(function(err) {
|
||||||
|
layer.close(loadIdx);
|
||||||
|
layer.msg('渲染失败: ' + err);
|
||||||
|
});
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user