mirror of
https://gitee.com/ulthon/ulthon_information.git
synced 2026-07-07 05:22:48 +08:00
1481 lines
53 KiB
JavaScript
1481 lines
53 KiB
JavaScript
/**
|
||
* PhoneImageEngine - 手机图片排版引擎
|
||
*
|
||
* 将文章HTML内容自动分页并渲染为手机尺寸图片
|
||
* 依赖: jQuery, html2canvas (项目已有)
|
||
*
|
||
* 渲染宽度540px, html2canvas scale=2 输出1080px
|
||
* 小红书: 540x720 (输出1080x1440)
|
||
* 抖音: 540x960 (输出1080x1920)
|
||
*/
|
||
var PhoneImageEngine = (function () {
|
||
|
||
// ===== 配置 =====
|
||
var config = {
|
||
size: 'xiaohongshu',
|
||
fontSize: 14,
|
||
watermark: '',
|
||
pageAlignments: {}, // key=页码(1-based), value='top'|'center'
|
||
sizes: {
|
||
xiaohongshu: { width: 540, height: 720 },
|
||
douyin: { width: 540, height: 960 }
|
||
},
|
||
contentPadding: 20
|
||
};
|
||
|
||
// ===== 文章数据 =====
|
||
var postData = {
|
||
id: 0,
|
||
title: '',
|
||
desc: '',
|
||
coverText: '', // 封面文案
|
||
content_html: '',
|
||
poster: '',
|
||
author_name: '',
|
||
create_time: '',
|
||
category_name: ''
|
||
};
|
||
|
||
// ===== 分页结果 =====
|
||
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} userConfig - {size, fontSize, watermark}
|
||
*/
|
||
function init(options, userConfig) {
|
||
postData.id = options.postId || 0;
|
||
postData.title = options.title || '';
|
||
postData.desc = options.desc || '';
|
||
postData.coverText = options.coverText || '';
|
||
postData.content_html = options.contentHtml || '';
|
||
postData.poster = options.poster || '';
|
||
postData.author_name = options.authorName || '';
|
||
postData.create_time = options.createTime || '';
|
||
postData.category_name = options.categoryName || '';
|
||
|
||
if (userConfig) {
|
||
$.extend(config, userConfig);
|
||
}
|
||
|
||
// 内容流事件委托(只绑定一次)
|
||
$(document).off('click', '.break-inserter-btn');
|
||
$(document).off('click', '.remove-break-btn');
|
||
$(document).off('click', '.thumb-alignment-toggle');
|
||
|
||
$(document).on('click', '.break-inserter-btn', function () {
|
||
var afterIndex = parseInt($(this).parent().data('after-index'), 10);
|
||
insertPageBreak(afterIndex);
|
||
});
|
||
|
||
$(document).on('click', '.remove-break-btn', function () {
|
||
var index = parseInt($(this).data('index'), 10);
|
||
removePageBreak(index);
|
||
});
|
||
|
||
$(document).on('click', '.thumb-alignment-toggle', function () {
|
||
var pageNum = parseInt($(this).data('page-num'), 10);
|
||
var currentAlign = (config.pageAlignments && config.pageAlignments[pageNum]) || 'top';
|
||
var newAlign = currentAlign === 'top' ? 'center' : 'top';
|
||
|
||
setPageAlignment(pageNum, newAlign);
|
||
render();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 渲染排版预览 - 生成分页后的HTML
|
||
* 两遍遍历: 先算总页数, 再带页码生成
|
||
* @returns {jQuery Deferred} resolves with pages array
|
||
*/
|
||
function render() {
|
||
var deferred = $.Deferred();
|
||
pages = [];
|
||
var sizeConfig = config.sizes[config.size] || config.sizes.xiaohongshu;
|
||
var pageHeight = sizeConfig.height;
|
||
var contentAreaHeight = pageHeight - (config.contentPadding * 2);
|
||
|
||
var cleanHtml = preprocessContent(postData.content_html);
|
||
var blocks = parseHtmlToBlocks(cleanHtml);
|
||
|
||
// 封面页
|
||
pages.push(generateCoverPage(sizeConfig));
|
||
|
||
// 内容分页
|
||
var contentPages = paginateContent(blocks, contentAreaHeight, sizeConfig);
|
||
pages = pages.concat(contentPages);
|
||
|
||
// 尾页
|
||
pages.push(generateSummaryPage(sizeConfig, pages.length));
|
||
|
||
// 两遍遍历: 现在知道了总页数, 给每页补上 N/M 页码
|
||
var totalPages = pages.length;
|
||
for (var i = 0; i < pages.length; i++) {
|
||
if (pages[i].type === 'content') {
|
||
// 在页脚中添加 N/M 格式页码
|
||
pages[i].html = pages[i].html.replace(
|
||
'<span>' + pages[i].pageNum + '</span>',
|
||
'<span class="page-number-indicator">' + (i + 1) + '/' + totalPages + '</span>'
|
||
);
|
||
}
|
||
}
|
||
|
||
// 渲染内容流(中间栏)- 同步
|
||
renderContentFlow(blocks);
|
||
|
||
// 渲染缩略图(右侧)- 异步截图
|
||
renderThumbnails(sizeConfig).then(function () {
|
||
deferred.resolve(pages);
|
||
}).catch(function (err) {
|
||
deferred.reject(err);
|
||
});
|
||
|
||
return deferred.promise();
|
||
}
|
||
|
||
/**
|
||
* 预处理HTML内容
|
||
*/
|
||
function preprocessContent(html) {
|
||
if (!html) return '';
|
||
html = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
|
||
html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
||
// 清除空段落
|
||
html = html.replace(/<p[^>]*>\s*<\/p>/gi, '');
|
||
|
||
// 处理图片: 清除内联样式, 强制 max-width:100%
|
||
html = html.replace(/<img([^>]*?)>/gi, function (match, attrs) {
|
||
// 移除 style, width, height 内联属性
|
||
attrs = attrs.replace(/\s*style\s*=\s*"[^"]*"/gi, '');
|
||
attrs = attrs.replace(/\s*style\s*=\s*'[^']*'/gi, '');
|
||
attrs = attrs.replace(/\s*width\s*=\s*"[^"]*"/gi, '');
|
||
attrs = attrs.replace(/\s*width\s*=\s*'[^']*'/gi, '');
|
||
attrs = attrs.replace(/\s*height\s*=\s*"[^"]*"/gi, '');
|
||
attrs = attrs.replace(/\s*height\s*=\s*'[^']*'/gi, '');
|
||
return '<img' + attrs + ' style="max-width:100%;height:auto;display:block;margin:10px 0;">';
|
||
});
|
||
|
||
return html;
|
||
}
|
||
|
||
/**
|
||
* 将HTML解析为块级元素数组
|
||
* 每个块: { type, html, estimatedHeight }
|
||
*/
|
||
function parseHtmlToBlocks(html) {
|
||
var blocks = [];
|
||
if (!html) return blocks;
|
||
|
||
var $temp = $('<div>').html(html);
|
||
|
||
// 展平嵌套的包装元素 (div, section, article, main, header, footer)
|
||
// 将其内部内容提取为直接子元素
|
||
var wrapperTags = ['div', 'section', 'article', 'main', 'header', 'footer', 'span'];
|
||
var changed = true;
|
||
var maxIterations = 10;
|
||
while (changed && maxIterations > 0) {
|
||
changed = false;
|
||
maxIterations--;
|
||
$temp.children().each(function () {
|
||
var $el = $(this);
|
||
var tagName = $el.prop('tagName').toLowerCase();
|
||
if ($.inArray(tagName, wrapperTags) !== -1) {
|
||
// 将此包装元素的子内容替换自身
|
||
var $children = $el.children();
|
||
if ($children.length > 0) {
|
||
$el.replaceWith($children);
|
||
changed = true;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
var children = $temp.children();
|
||
|
||
if (children.length === 0) {
|
||
// 纯文本内容,按段落分割
|
||
var text = $temp.text().trim();
|
||
if (text) {
|
||
var paragraphs = text.split(/\n\s*\n|\n/);
|
||
for (var i = 0; i < paragraphs.length; i++) {
|
||
var p = paragraphs[i].trim();
|
||
if (p) {
|
||
blocks.push({
|
||
type: 'p',
|
||
html: '<p>' + p + '</p>',
|
||
estimatedHeight: estimateTextHeight(p, config.fontSize)
|
||
});
|
||
}
|
||
}
|
||
}
|
||
return blocks;
|
||
}
|
||
|
||
children.each(function () {
|
||
var $el = $(this);
|
||
var tagName = $el.prop('tagName').toLowerCase();
|
||
var block = {
|
||
type: tagName,
|
||
html: $el[0].outerHTML
|
||
};
|
||
|
||
switch (tagName) {
|
||
case 'hr':
|
||
block.type = 'page-break';
|
||
block.html = '';
|
||
block.estimatedHeight = 0;
|
||
break;
|
||
case 'h1':
|
||
case 'h2':
|
||
case 'h3':
|
||
case 'h4':
|
||
case 'h5':
|
||
case 'h6':
|
||
block.type = tagName;
|
||
block.estimatedHeight = estimateHeadingHeight(tagName, $el.text());
|
||
break;
|
||
case 'table':
|
||
block.type = 'table';
|
||
block.html = $el[0].outerHTML;
|
||
block.estimatedHeight = estimateTableHeight($el);
|
||
break;
|
||
case 'img':
|
||
block.estimatedHeight = estimateImageHeight($el);
|
||
block.$img = $el;
|
||
break;
|
||
case 'ul':
|
||
case 'ol':
|
||
block.estimatedHeight = estimateListHeight($el);
|
||
break;
|
||
case 'blockquote':
|
||
block.estimatedHeight = estimateBlockquoteHeight($el);
|
||
break;
|
||
case 'p':
|
||
default:
|
||
block.type = 'p';
|
||
block.estimatedHeight = estimateTextHeight($el.text(), config.fontSize);
|
||
break;
|
||
}
|
||
|
||
blocks.push(block);
|
||
});
|
||
|
||
return blocks;
|
||
}
|
||
|
||
// ===== 高度估算 =====
|
||
|
||
/**
|
||
* 获取内容区可用宽度 (540 - 40px padding)
|
||
*/
|
||
function getContentWidth() {
|
||
return 500;
|
||
}
|
||
|
||
/**
|
||
* 估算文本高度
|
||
* @param {string} text
|
||
* @param {number} fontSize
|
||
* @returns {number} px
|
||
*/
|
||
function estimateTextHeight(text, fontSize) {
|
||
var charCount = text.length;
|
||
if (charCount === 0) return 0;
|
||
var charsPerLine = Math.floor(getContentWidth() / (fontSize * 1.0));
|
||
if (charsPerLine < 1) charsPerLine = 1;
|
||
var lines = Math.ceil(charCount / charsPerLine);
|
||
return lines * fontSize * 1.8 + 10; // 1.8行高 + 10px段落间距
|
||
}
|
||
|
||
/**
|
||
* 估算标题高度
|
||
*/
|
||
function estimateHeadingHeight(tag, text) {
|
||
var sizeMap = { h1: 28, h2: 24, h3: 20, h4: 18, h5: 16, h6: 14 };
|
||
var hSize = sizeMap[tag] || 20;
|
||
var charsPerLine = Math.floor(getContentWidth() / (hSize * 1.0));
|
||
if (charsPerLine < 1) charsPerLine = 1;
|
||
var lines = Math.ceil(text.length / charsPerLine);
|
||
return lines * hSize * 1.4 + 20; // heading行高1.4 + 20px上下间距
|
||
}
|
||
|
||
/**
|
||
* 估算图片高度
|
||
* 默认按内容区宽度的60%估算
|
||
*/
|
||
function estimateImageHeight($img) {
|
||
var w = parseInt($img.attr('width'), 10);
|
||
var h = parseInt($img.attr('height'), 10);
|
||
if (w > 0 && h > 0) {
|
||
// 按比例缩放到内容宽度
|
||
var ratio = getContentWidth() / w;
|
||
return Math.round(h * ratio) + 10;
|
||
}
|
||
// 无尺寸信息时默认300px
|
||
return 310;
|
||
}
|
||
|
||
/**
|
||
* 估算列表高度
|
||
*/
|
||
function estimateListHeight($list) {
|
||
var items = $list.find('li').length;
|
||
return items * (config.fontSize * 1.8 + 5) + 15;
|
||
}
|
||
|
||
/**
|
||
* 估算引用块高度
|
||
*/
|
||
function estimateBlockquoteHeight($bq) {
|
||
var text = $bq.text();
|
||
return estimateTextHeight(text, config.fontSize) + 10;
|
||
}
|
||
|
||
/**
|
||
* 估算表格高度
|
||
*/
|
||
function estimateTableHeight($table) {
|
||
var rows = $table.find('tr').length;
|
||
return rows * 36 + 20; // 36px per row (35px content + 1px border) + 20px margin
|
||
}
|
||
|
||
// ===== 分页核心算法 =====
|
||
|
||
/**
|
||
* 将块级元素按高度累加,超过可用高度时分页
|
||
* 支持 <hr> 强制分页和超大块拆分(超长文本拆成多段)
|
||
*/
|
||
function paginateContent(blocks, contentAreaHeight, sizeConfig) {
|
||
var contentPages = [];
|
||
var currentPageBlocks = [];
|
||
var currentHeight = 0;
|
||
var pageNumber = 1;
|
||
|
||
for (var i = 0; i < blocks.length; i++) {
|
||
var block = blocks[i];
|
||
|
||
// 强制分页标记
|
||
if (block.type === 'page-break') {
|
||
// 结束当前页
|
||
if (currentPageBlocks.length > 0) {
|
||
contentPages.push(generateContentPage(
|
||
currentPageBlocks, pageNumber, sizeConfig, false
|
||
));
|
||
currentPageBlocks = [];
|
||
currentHeight = 0;
|
||
pageNumber++;
|
||
}
|
||
continue; // 跳过这个 page-break 块本身
|
||
}
|
||
|
||
// 超大块处理:单个块超过整页高度时需要拆分
|
||
if (block.estimatedHeight > contentAreaHeight) {
|
||
// 先把当前页已有的内容推出去
|
||
if (currentPageBlocks.length > 0) {
|
||
contentPages.push(generateContentPage(
|
||
currentPageBlocks, pageNumber, sizeConfig, false
|
||
));
|
||
currentPageBlocks = [];
|
||
currentHeight = 0;
|
||
pageNumber++;
|
||
}
|
||
|
||
// 尝试拆分超大块
|
||
var splitBlocks = splitOversizedBlock(block, contentAreaHeight);
|
||
for (var s = 0; s < splitBlocks.length; s++) {
|
||
var sb = splitBlocks[s];
|
||
if (currentHeight + sb.estimatedHeight > contentAreaHeight && currentPageBlocks.length > 0) {
|
||
contentPages.push(generateContentPage(
|
||
currentPageBlocks, pageNumber, sizeConfig, false
|
||
));
|
||
currentPageBlocks = [];
|
||
currentHeight = 0;
|
||
pageNumber++;
|
||
}
|
||
currentPageBlocks.push(sb);
|
||
currentHeight += sb.estimatedHeight;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// 正常块:判断是否需要换页
|
||
if (currentHeight + block.estimatedHeight > contentAreaHeight && currentPageBlocks.length > 0) {
|
||
contentPages.push(generateContentPage(
|
||
currentPageBlocks, pageNumber, sizeConfig, false
|
||
));
|
||
currentPageBlocks = [];
|
||
currentHeight = 0;
|
||
pageNumber++;
|
||
}
|
||
|
||
currentPageBlocks.push(block);
|
||
currentHeight += block.estimatedHeight;
|
||
}
|
||
|
||
// 最后一页
|
||
if (currentPageBlocks.length > 0) {
|
||
contentPages.push(generateContentPage(
|
||
currentPageBlocks, pageNumber, sizeConfig, true
|
||
));
|
||
}
|
||
|
||
return contentPages;
|
||
}
|
||
|
||
/**
|
||
* 拆分超大块(超长段落文本拆成多段)
|
||
* @param {Object} block - 块级元素
|
||
* @param {number} pageHeight - 可用内容高度
|
||
* @returns {Array} 拆分后的块数组
|
||
*/
|
||
function splitOversizedBlock(block, pageHeight) {
|
||
// 图片块不拆分,保留原样(会被截断但保持完整)
|
||
if (block.type === 'img') {
|
||
return [block];
|
||
}
|
||
|
||
// 表格块不拆分,保留原样
|
||
if (block.type === 'table') {
|
||
return [block];
|
||
}
|
||
|
||
// 文本类块:按句子拆分
|
||
var text = '';
|
||
var wrapperTag = 'p';
|
||
if (block.type === 'blockquote') {
|
||
text = $(block.html).text();
|
||
wrapperTag = 'blockquote';
|
||
} else {
|
||
text = $(block.html).text();
|
||
}
|
||
|
||
if (!text || text.length === 0) {
|
||
return [block];
|
||
}
|
||
|
||
// 按句子拆分:句号、问号、叹号、换行
|
||
var sentences = text.split(/(?<=[。!?\n])/);
|
||
if (sentences.length <= 1) {
|
||
// 无法按句子拆分,按固定字符数拆分
|
||
sentences = [];
|
||
var chunkSize = Math.floor(getContentWidth() / config.fontSize) *
|
||
Math.floor(pageHeight / (config.fontSize * 1.8));
|
||
if (chunkSize < 10) chunkSize = 10;
|
||
for (var ci = 0; ci < text.length; ci += chunkSize) {
|
||
sentences.push(text.substring(ci, ci + chunkSize));
|
||
}
|
||
}
|
||
|
||
var result = [];
|
||
for (var j = 0; j < sentences.length; j++) {
|
||
var s = sentences[j].trim();
|
||
if (!s) continue;
|
||
var h = estimateTextHeight(s, config.fontSize);
|
||
result.push({
|
||
type: wrapperTag === 'blockquote' ? 'blockquote' : 'p',
|
||
html: '<' + wrapperTag + '>' + s + '</' + wrapperTag + '>',
|
||
estimatedHeight: h
|
||
});
|
||
}
|
||
|
||
return result.length > 0 ? result : [block];
|
||
}
|
||
|
||
// ===== 页面HTML生成 =====
|
||
|
||
/**
|
||
* 检查是否有有效的封面图(排除默认头像占位图)
|
||
*/
|
||
function hasValidPoster() {
|
||
if (!postData.poster) return false;
|
||
// 排除默认头像占位图
|
||
var defaultPatterns = ['/static/images/avatar.png', '/static/images/avatar.jpeg', '/static/images/avatar.jpg'];
|
||
for (var i = 0; i < defaultPatterns.length; i++) {
|
||
if (postData.poster.indexOf(defaultPatterns[i]) !== -1) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 生成封面页HTML
|
||
*/
|
||
function generateCoverPage(sizeConfig) {
|
||
var hasCover = hasValidPoster();
|
||
var html = '<div class="phone-image-page page-cover';
|
||
if (hasCover) {
|
||
html += ' has-cover-image';
|
||
} else {
|
||
html += ' no-cover-image';
|
||
}
|
||
html += '" style="width:' + sizeConfig.width + 'px;height:' + sizeConfig.height + 'px;">';
|
||
|
||
if (hasCover) {
|
||
html += '<img class="cover-image" src="' + postData.poster + '" alt="">';
|
||
html += '<div class="cover-title">' + escapeHtml(postData.title) + '</div>';
|
||
if (postData.desc) {
|
||
html += '<div class="cover-subtitle">' + escapeHtml(postData.desc) + '</div>';
|
||
}
|
||
// 封面文案(有封面图)
|
||
if (postData.coverText) {
|
||
html += '<div style="font-size:14px;color:#1890ff;margin-top:8px;">' + escapeHtml(postData.coverText) + '</div>';
|
||
}
|
||
html += '<div class="cover-meta">';
|
||
if (postData.category_name) {
|
||
html += escapeHtml(postData.category_name) + ' | ';
|
||
}
|
||
if (postData.author_name) {
|
||
html += escapeHtml(postData.author_name) + ' | ';
|
||
}
|
||
if (postData.create_time) {
|
||
html += postData.create_time;
|
||
}
|
||
html += '</div>';
|
||
} else {
|
||
// 无封面图: 大标题+摘要+装饰线条排版
|
||
html += '<div class="cover-decor-line cover-decor-top"></div>';
|
||
html += '<div class="cover-no-img-content">';
|
||
html += '<div class="cover-no-img-title">' + escapeHtml(postData.title) + '</div>';
|
||
html += '<div class="cover-decor-divider"></div>';
|
||
if (postData.desc) {
|
||
html += '<div class="cover-no-img-desc">' + escapeHtml(postData.desc) + '</div>';
|
||
}
|
||
// 封面文案(无封面图)
|
||
if (postData.coverText) {
|
||
html += '<div style="font-size:14px;color:#1890ff;margin-top:10px;padding:0 30px;word-break:break-word;">' + escapeHtml(postData.coverText) + '</div>';
|
||
}
|
||
html += '<div class="cover-no-img-meta">';
|
||
if (postData.category_name) {
|
||
html += '<span>' + escapeHtml(postData.category_name) + '</span>';
|
||
}
|
||
if (postData.author_name) {
|
||
html += '<span>' + escapeHtml(postData.author_name) + '</span>';
|
||
}
|
||
if (postData.create_time) {
|
||
html += '<span>' + postData.create_time + '</span>';
|
||
}
|
||
html += '</div>';
|
||
html += '</div>';
|
||
html += '<div class="cover-decor-line cover-decor-bottom"></div>';
|
||
}
|
||
|
||
html += '</div>';
|
||
|
||
return { type: 'cover', html: html };
|
||
}
|
||
|
||
/**
|
||
* 生成内容页HTML(含逐页对齐支持)
|
||
*/
|
||
function generateContentPage(blocks, pageNum, sizeConfig, isLast) {
|
||
// 读取逐页对齐配置
|
||
var alignment = (config.pageAlignments && config.pageAlignments[pageNum]) || 'top';
|
||
var valignClass = alignment === 'center' ? ' valign-center' : '';
|
||
|
||
var html = '<div class="phone-image-page page-body' + valignClass + '" style="width:' +
|
||
sizeConfig.width + 'px;height:' + sizeConfig.height + 'px;">';
|
||
|
||
// 页头(仅第一页内容页显示标题)
|
||
if (pageNum === 1) {
|
||
html += '<div class="page-header">';
|
||
html += '<div class="page-title">' + escapeHtml(postData.title) + '</div>';
|
||
html += '</div>';
|
||
}
|
||
|
||
// 正文内容区
|
||
html += '<div class="page-content">';
|
||
for (var i = 0; i < blocks.length; i++) {
|
||
html += blocks[i].html;
|
||
}
|
||
html += '</div>';
|
||
|
||
// 页脚 - 占位,render() 中会替换页码
|
||
html += '<div class="page-footer">';
|
||
html += '<span>' + escapeHtml(postData.author_name || '') + '</span>';
|
||
html += '<span>' + pageNum + '</span>';
|
||
html += '</div>';
|
||
|
||
html += '</div>';
|
||
return { type: 'content', html: html, pageNum: pageNum };
|
||
}
|
||
|
||
/**
|
||
* 生成尾页/总结页HTML
|
||
*/
|
||
function generateSummaryPage(sizeConfig, totalPages) {
|
||
var html = '<div class="phone-image-page page-summary" style="width:' +
|
||
sizeConfig.width + 'px;height:' + sizeConfig.height + 'px;">';
|
||
html += '<div class="summary-title">感谢阅读</div>';
|
||
html += '<div class="summary-text">' + escapeHtml(postData.title) + '</div>';
|
||
|
||
if (postData.desc) {
|
||
html += '<div class="summary-text" style="font-size:12px;">' + escapeHtml(postData.desc) + '</div>';
|
||
}
|
||
|
||
html += '<div class="summary-footer">';
|
||
html += '共 ' + totalPages + ' 页';
|
||
if (postData.author_name) {
|
||
html += ' | ' + escapeHtml(postData.author_name);
|
||
}
|
||
html += '</div>';
|
||
html += '</div>';
|
||
|
||
return { type: 'summary', html: html };
|
||
}
|
||
|
||
// ===== DOM渲染 =====
|
||
|
||
/**
|
||
* 将 #render-staging 中的代码块转为图片
|
||
* 使用 prismjs 高亮 + html2canvas 截图替换
|
||
* @returns {jQuery Deferred} resolves when all code blocks are converted
|
||
*/
|
||
function convertCodeBlocks() {
|
||
var deferred = $.Deferred();
|
||
var $staging = $('#render-staging');
|
||
|
||
// 检查 Prism 是否可用
|
||
if (typeof Prism === 'undefined') {
|
||
deferred.resolve();
|
||
return deferred.promise();
|
||
}
|
||
|
||
// 给没有语言类的代码块添加默认语言
|
||
$staging.find('pre > code:not([class*="language-"])').addClass('language-plaintext');
|
||
|
||
// 高亮所有代码块
|
||
$staging.find('pre code').each(function () {
|
||
Prism.highlightElement(this);
|
||
});
|
||
|
||
// 找到所有代码块,逐个截图替换
|
||
var $preBlocks = $staging.find('pre');
|
||
if ($preBlocks.length === 0) {
|
||
deferred.resolve();
|
||
return deferred.promise();
|
||
}
|
||
|
||
var idx = 0;
|
||
var total = $preBlocks.length;
|
||
|
||
function convertNext() {
|
||
if (idx >= total) {
|
||
deferred.resolve();
|
||
return;
|
||
}
|
||
|
||
var $pre = $preBlocks.eq(idx);
|
||
html2canvas($pre[0], {
|
||
scale: 2,
|
||
useCORS: true,
|
||
backgroundColor: '#f5f5f5',
|
||
logging: false
|
||
}).then(function (canvas) {
|
||
var imgData = canvas.toDataURL('image/jpeg', 0.92);
|
||
var $img = $('<img>');
|
||
$img.attr('src', imgData);
|
||
$img.css({
|
||
maxWidth: '100%',
|
||
height: 'auto',
|
||
display: 'block',
|
||
margin: '10px 0'
|
||
});
|
||
$pre.replaceWith($img);
|
||
idx++;
|
||
convertNext();
|
||
}).catch(function () {
|
||
// 截图失败则保留原始代码块
|
||
idx++;
|
||
convertNext();
|
||
});
|
||
}
|
||
|
||
convertNext();
|
||
return deferred.promise();
|
||
}
|
||
|
||
/**
|
||
* 将 #render-staging 中的表格转为图片
|
||
* 使用 html2canvas 截图替换(与 convertCodeBlocks 模式相同)
|
||
* @returns {jQuery Deferred} resolves when all tables are converted
|
||
*/
|
||
function convertTables() {
|
||
var deferred = $.Deferred();
|
||
var $staging = $('#render-staging');
|
||
|
||
var $tables = $staging.find('.page-content table');
|
||
if ($tables.length === 0) {
|
||
deferred.resolve();
|
||
return deferred.promise();
|
||
}
|
||
|
||
var idx = 0;
|
||
var total = $tables.length;
|
||
|
||
function convertNext() {
|
||
if (idx >= total) {
|
||
deferred.resolve();
|
||
return;
|
||
}
|
||
|
||
var $table = $tables.eq(idx);
|
||
html2canvas($table[0], {
|
||
scale: 2,
|
||
useCORS: true,
|
||
backgroundColor: '#ffffff',
|
||
logging: false
|
||
}).then(function (canvas) {
|
||
var imgData = canvas.toDataURL('image/jpeg', 0.92);
|
||
var $img = $('<img>');
|
||
$img.attr('src', imgData);
|
||
$img.css({
|
||
maxWidth: '100%',
|
||
height: 'auto',
|
||
display: 'block',
|
||
margin: '10px 0'
|
||
});
|
||
$table.replaceWith($img);
|
||
idx++;
|
||
convertNext();
|
||
}).catch(function () {
|
||
idx++;
|
||
convertNext();
|
||
});
|
||
}
|
||
|
||
convertNext();
|
||
return deferred.promise();
|
||
}
|
||
|
||
// ===== 纯图片页优化 =====
|
||
|
||
/**
|
||
* 检测staging区域中的页面元素是否为纯图片页
|
||
* 纯图片页: .page-body 内 .page-content 只含1个img且无文字内容
|
||
* @param {jQuery} $pageElem - .phone-image-page 元素
|
||
* @returns {boolean}
|
||
*/
|
||
function isPureImagePage($pageElem) {
|
||
// 仅内容页
|
||
if (!$pageElem.hasClass('page-body')) return false;
|
||
var $content = $pageElem.find('.page-content');
|
||
if (!$content.length) return false;
|
||
|
||
// 检查是否有文字内容(排除纯空白文本节点)
|
||
var textContent = $content.text().replace(/\s/g, '');
|
||
if (textContent.length > 0) return false;
|
||
|
||
// 检查img数量: 必须恰好1个img
|
||
var $imgs = $content.find('img');
|
||
if ($imgs.length !== 1) return false;
|
||
|
||
// img必须有有效src
|
||
var src = $imgs.first().attr('src');
|
||
return src && src.length > 0;
|
||
}
|
||
|
||
/**
|
||
* 从纯图片页元素中获取img的src
|
||
* @param {jQuery} $pageElem
|
||
* @returns {string}
|
||
*/
|
||
function getPureImageSrc($pageElem) {
|
||
var $img = $pageElem.find('.page-content img').first();
|
||
return $img.attr('src') || '';
|
||
}
|
||
|
||
/**
|
||
* 将原始图片绘制到canvas(用于高质量保存路径)
|
||
* @param {string} src - 图片src
|
||
* @param {number} width - 目标宽度
|
||
* @param {number} height - 目标高度
|
||
* @returns {jQuery Deferred} resolves with canvas
|
||
*/
|
||
function renderPureImageToCanvas(src, width, height) {
|
||
var deferred = $.Deferred();
|
||
var img = new Image();
|
||
img.crossOrigin = 'anonymous';
|
||
img.onload = function () {
|
||
if (img.naturalWidth === 0 || img.naturalHeight === 0) {
|
||
deferred.reject('Image has zero dimensions');
|
||
return;
|
||
}
|
||
var canvas = document.createElement('canvas');
|
||
canvas.width = width * 2; // scale:2 高质量
|
||
canvas.height = height * 2;
|
||
var ctx = canvas.getContext('2d');
|
||
// 白色背景
|
||
ctx.fillStyle = '#ffffff';
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
// 缩放绘制
|
||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||
deferred.resolve(canvas);
|
||
};
|
||
img.onerror = function () {
|
||
deferred.reject('Image load failed');
|
||
};
|
||
img.src = src;
|
||
return deferred.promise();
|
||
}
|
||
|
||
/**
|
||
* 渲染到隐藏区域 → 截图 → 显示缩略图
|
||
* @param {Object} sizeConfig
|
||
* @returns {jQuery Deferred} resolves with pages array
|
||
*/
|
||
function renderThumbnails(sizeConfig) {
|
||
var deferred = $.Deferred();
|
||
ensureStaging();
|
||
|
||
var $staging = $('#render-staging');
|
||
$staging.empty();
|
||
|
||
// 渲染所有页面到隐藏区域(使用wrapper以便html2canvas正确渲染)
|
||
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);
|
||
|
||
// 等待一帧确保 DOM 渲染完成
|
||
requestAnimationFrame(function () {
|
||
// html2canvas会跳过visibility:hidden的元素,临时切换为可见
|
||
$staging.css({ visibility: 'visible' });
|
||
// 先转换代码块为图片
|
||
convertCodeBlocks().then(function () {
|
||
return convertTables();
|
||
}).then(function () {
|
||
var $pageElems = $staging.find('.phone-image-page');
|
||
if ($pageElems.length === 0) {
|
||
$staging.css({ visibility: 'hidden' });
|
||
displayThumbnails([], sizeConfig);
|
||
deferred.resolve(pages);
|
||
return;
|
||
}
|
||
|
||
// 串行截图每一页
|
||
var thumbnailDataUrls = [];
|
||
var idx = 0;
|
||
var total = $pageElems.length;
|
||
|
||
function captureNext() {
|
||
if (idx >= total) {
|
||
$staging.empty();
|
||
$staging.css({ visibility: 'hidden' });
|
||
displayThumbnails(thumbnailDataUrls, sizeConfig);
|
||
deferred.resolve(pages);
|
||
return;
|
||
}
|
||
|
||
var $elem = $pageElems.eq(idx);
|
||
// 纯图片页优化: 跳过html2canvas,直接用原图src
|
||
if (isPureImagePage($elem)) {
|
||
var pureSrc = getPureImageSrc($elem);
|
||
if (pureSrc) {
|
||
thumbnailDataUrls.push(pureSrc);
|
||
idx++;
|
||
captureNext();
|
||
return;
|
||
}
|
||
}
|
||
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) {
|
||
$staging.css({ visibility: 'hidden' });
|
||
deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err);
|
||
});
|
||
}
|
||
|
||
captureNext();
|
||
}).catch(function () {
|
||
// 代码块转换失败,继续转换表格并截图流程
|
||
convertTables().then(function () {
|
||
var $pageElems = $staging.find('.phone-image-page');
|
||
if ($pageElems.length === 0) {
|
||
$staging.css({ visibility: 'hidden' });
|
||
displayThumbnails([], sizeConfig);
|
||
deferred.resolve(pages);
|
||
return;
|
||
}
|
||
|
||
var thumbnailDataUrls = [];
|
||
var idx = 0;
|
||
var total = $pageElems.length;
|
||
|
||
function captureNext() {
|
||
if (idx >= total) {
|
||
$staging.empty();
|
||
$staging.css({ visibility: 'hidden' });
|
||
displayThumbnails(thumbnailDataUrls, sizeConfig);
|
||
deferred.resolve(pages);
|
||
return;
|
||
}
|
||
|
||
var $elem = $pageElems.eq(idx);
|
||
// 纯图片页优化: 跳过html2canvas,直接用原图src
|
||
if (isPureImagePage($elem)) {
|
||
var pureSrc = getPureImageSrc($elem);
|
||
if (pureSrc) {
|
||
thumbnailDataUrls.push(pureSrc);
|
||
idx++;
|
||
captureNext();
|
||
return;
|
||
}
|
||
}
|
||
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) {
|
||
$staging.css({ visibility: 'hidden' });
|
||
deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err);
|
||
});
|
||
}
|
||
|
||
captureNext();
|
||
});
|
||
});
|
||
});
|
||
|
||
return deferred.promise();
|
||
}
|
||
|
||
/**
|
||
* 在右侧显示缩略图
|
||
* @param {Array} dataUrls - base64 图片数据
|
||
* @param {Object} sizeConfig
|
||
*/
|
||
function displayThumbnails(dataUrls, sizeConfig) {
|
||
var $preview = $('#paginated-preview');
|
||
if (!$preview.length) return;
|
||
|
||
$preview.empty();
|
||
if (dataUrls.length === 0) 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 = $('<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);
|
||
|
||
// 对齐按钮(仅内容页)
|
||
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 $toggle = $('<button class="thumb-alignment-toggle' + (isActiveCenter ? ' active-center' : '') + '" data-page-index="' + i + '" data-page-num="' + pageNum + '">' +
|
||
(isActiveCenter ? '\u2195' : '\u2191') +
|
||
'</button>');
|
||
$item.append($toggle);
|
||
}
|
||
|
||
$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 () {
|
||
// html2canvas会跳过visibility:hidden的元素,临时切换为可见
|
||
$staging.css({ visibility: 'visible' });
|
||
// 先转换代码块为图片
|
||
convertCodeBlocks().then(function () {
|
||
return convertTables();
|
||
}).then(function () {
|
||
var $pageElems = $staging.find('.phone-image-page');
|
||
if ($pageElems.length === 0) {
|
||
$staging.css({ visibility: 'hidden' });
|
||
deferred.reject('没有可渲染的页面');
|
||
return;
|
||
}
|
||
|
||
var canvases = [];
|
||
var idx = 0;
|
||
var total = $pageElems.length;
|
||
|
||
function captureNext() {
|
||
if (idx >= total) {
|
||
$staging.empty();
|
||
$staging.css({ visibility: 'hidden' });
|
||
deferred.resolve(canvases);
|
||
return;
|
||
}
|
||
|
||
var $elem = $pageElems.eq(idx);
|
||
// 纯图片页优化: 直接绘制原图到canvas
|
||
if (isPureImagePage($elem)) {
|
||
var pureSrc = getPureImageSrc($elem);
|
||
if (pureSrc) {
|
||
renderPureImageToCanvas(pureSrc, sizeConfig.width, sizeConfig.height).then(function (canvas) {
|
||
canvases.push(canvas);
|
||
idx++;
|
||
captureNext();
|
||
}).catch(function () {
|
||
// 加载失败,回退到html2canvas
|
||
html2canvas($elem[0], {
|
||
scale: 2,
|
||
useCORS: true,
|
||
backgroundColor: '#ffffff',
|
||
width: $elem.outerWidth(),
|
||
height: $elem.outerHeight(),
|
||
logging: false
|
||
}).then(function (canvas) {
|
||
canvases.push(canvas);
|
||
idx++;
|
||
captureNext();
|
||
}).catch(function (err) {
|
||
$staging.css({ visibility: 'hidden' });
|
||
deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err);
|
||
});
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
html2canvas($elem[0], {
|
||
scale: 2,
|
||
useCORS: true,
|
||
backgroundColor: '#ffffff',
|
||
width: $elem.outerWidth(),
|
||
height: $elem.outerHeight(),
|
||
logging: false
|
||
}).then(function (canvas) {
|
||
canvases.push(canvas);
|
||
idx++;
|
||
captureNext();
|
||
}).catch(function (err) {
|
||
$staging.css({ visibility: 'hidden' });
|
||
deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err);
|
||
});
|
||
}
|
||
|
||
captureNext();
|
||
}).catch(function () {
|
||
// 代码块转换失败,继续转换表格并截图流程
|
||
convertTables().then(function () {
|
||
var $pageElems = $staging.find('.phone-image-page');
|
||
if ($pageElems.length === 0) {
|
||
$staging.css({ visibility: 'hidden' });
|
||
deferred.reject('没有可渲染的页面');
|
||
return;
|
||
}
|
||
|
||
var canvases = [];
|
||
var idx = 0;
|
||
var total = $pageElems.length;
|
||
|
||
function captureNext() {
|
||
if (idx >= total) {
|
||
$staging.empty();
|
||
$staging.css({ visibility: 'hidden' });
|
||
deferred.resolve(canvases);
|
||
return;
|
||
}
|
||
|
||
var $elem = $pageElems.eq(idx);
|
||
// 纯图片页优化: 直接绘制原图到canvas
|
||
if (isPureImagePage($elem)) {
|
||
var pureSrc = getPureImageSrc($elem);
|
||
if (pureSrc) {
|
||
renderPureImageToCanvas(pureSrc, sizeConfig.width, sizeConfig.height).then(function (canvas) {
|
||
canvases.push(canvas);
|
||
idx++;
|
||
captureNext();
|
||
}).catch(function () {
|
||
// 加载失败,回退到html2canvas
|
||
html2canvas($elem[0], {
|
||
scale: 2,
|
||
useCORS: true,
|
||
backgroundColor: '#ffffff',
|
||
width: $elem.outerWidth(),
|
||
height: $elem.outerHeight(),
|
||
logging: false
|
||
}).then(function (canvas) {
|
||
canvases.push(canvas);
|
||
idx++;
|
||
captureNext();
|
||
}).catch(function (err) {
|
||
$staging.css({ visibility: 'hidden' });
|
||
deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err);
|
||
});
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
html2canvas($elem[0], {
|
||
scale: 2,
|
||
useCORS: true,
|
||
backgroundColor: '#ffffff',
|
||
width: $elem.outerWidth(),
|
||
height: $elem.outerHeight(),
|
||
logging: false
|
||
}).then(function (canvas) {
|
||
canvases.push(canvas);
|
||
idx++;
|
||
captureNext();
|
||
}).catch(function (err) {
|
||
$staging.css({ visibility: 'hidden' });
|
||
deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err);
|
||
});
|
||
}
|
||
|
||
captureNext();
|
||
});
|
||
});
|
||
});
|
||
|
||
return deferred.promise();
|
||
}
|
||
|
||
/**
|
||
* 渲染内容流到 #content-flow 容器
|
||
* @param {Array} blocks - parseHtmlToBlocks 返回的块数组
|
||
*/
|
||
function renderContentFlow(blocks) {
|
||
var $flow = $('#content-flow');
|
||
if (!$flow.length) return;
|
||
|
||
$flow.empty();
|
||
|
||
for (var i = 0; i < blocks.length; i++) {
|
||
var block = blocks[i];
|
||
|
||
// 分页标记
|
||
if (block.type === 'page-break') {
|
||
var $marker = $('<div class="page-break-marker">' +
|
||
'<span class="break-marker-label">-- 分页标记 --</span>' +
|
||
'<button class="remove-break-btn" data-index="' + i + '" title="删除分页">×</button>' +
|
||
'</div>');
|
||
$flow.append($marker);
|
||
continue;
|
||
}
|
||
|
||
// 内容块
|
||
var $block = $('<div class="content-flow-block" data-index="' + i + '">' + block.html + '</div>');
|
||
$flow.append($block);
|
||
|
||
// 块与块之间的分页插入区域(不在最后一个块之后添加)
|
||
if (i < blocks.length - 1) {
|
||
var $inserter = $('<div class="break-inserter" data-after-index="' + i + '">' +
|
||
'<button class="break-inserter-btn" title="插入分页">+</button>' +
|
||
'</div>');
|
||
$flow.append($inserter);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 在指定位置插入分页标记
|
||
* 修改 postData.content_html,在对应位置插入 <hr>
|
||
* @param {number} blockIndex - 在哪个块之后插入 (对应 break-inserter 的 data-after-index)
|
||
*/
|
||
function insertPageBreak(blockIndex) {
|
||
var cleanHtml = preprocessContent(postData.content_html);
|
||
var $temp = $('<div>').html(cleanHtml);
|
||
var children = $temp.children();
|
||
|
||
// 在 blockIndex 位置的元素之后插入 <hr>
|
||
if (blockIndex >= 0 && blockIndex < children.length) {
|
||
$(children[blockIndex]).after('<hr>');
|
||
} else {
|
||
$temp.append('<hr>');
|
||
}
|
||
|
||
postData.content_html = $temp.html();
|
||
|
||
// 重新渲染
|
||
render();
|
||
}
|
||
|
||
/**
|
||
* 删除指定位置的分页标记
|
||
* @param {number} blockIndex - 分页标记的 data-index
|
||
*/
|
||
function removePageBreak(blockIndex) {
|
||
// 统计这是第几个 page-break
|
||
var cleanHtml = preprocessContent(postData.content_html);
|
||
var $temp = $('<div>').html(cleanHtml);
|
||
var hrElements = $temp.find('hr');
|
||
|
||
// 在 blocks 数组中找到这个 page-break 是第几个
|
||
var breakOrder = 0;
|
||
var currentBlocks = parseHtmlToBlocks(cleanHtml);
|
||
for (var i = 0; i < currentBlocks.length && i < blockIndex; i++) {
|
||
if (currentBlocks[i].type === 'page-break') breakOrder++;
|
||
}
|
||
|
||
if (breakOrder < hrElements.length) {
|
||
$(hrElements[breakOrder]).remove();
|
||
postData.content_html = $temp.html();
|
||
render();
|
||
}
|
||
}
|
||
|
||
// ===== 图片生成 =====
|
||
|
||
/**
|
||
* 逐页截图生成图片(使用隐藏渲染区域高质量截图)
|
||
* @param {Function} [onProgress] 进度回调 function(currentIndex, totalPages, canvas)
|
||
* @returns {Promise} jQuery Deferred, resolves with array of canvas objects
|
||
*/
|
||
function generateImages(onProgress) {
|
||
var deferred = $.Deferred();
|
||
|
||
if (pages.length === 0) {
|
||
deferred.reject('没有可渲染的页面');
|
||
return deferred.promise();
|
||
}
|
||
|
||
if (onProgress) onProgress(0, pages.length, null);
|
||
|
||
capturePagesFromStaging().then(function (canvases) {
|
||
if (onProgress) onProgress(canvases.length, canvases.length, null);
|
||
deferred.resolve(canvases);
|
||
}).catch(function (err) {
|
||
deferred.reject(err);
|
||
});
|
||
|
||
return deferred.promise();
|
||
}
|
||
|
||
/**
|
||
* 将canvas转为base64并保存到服务端
|
||
* @param {number} postId 文章ID
|
||
* @param {Object} saveConfig 配置信息
|
||
* @param {Function} [onProgress] 生成进度回调 function(current, total, canvas)
|
||
* @returns {Promise} jQuery Deferred
|
||
*/
|
||
function saveImages(postId, saveConfig, onProgress) {
|
||
var deferred = $.Deferred();
|
||
|
||
capturePagesFromStaging().then(function (canvases) {
|
||
if (onProgress) onProgress(canvases.length, canvases.length, null);
|
||
var pagesData = [];
|
||
for (var i = 0; i < canvases.length; i++) {
|
||
pagesData.push(canvases[i].toDataURL('image/jpeg', 0.92));
|
||
}
|
||
|
||
$.ajax({
|
||
url: '/index.php/admin/post/savePostOutput',
|
||
type: 'POST',
|
||
data: JSON.stringify({
|
||
post_id: postId,
|
||
output_type: 'phone_image',
|
||
config: saveConfig || config,
|
||
content_html: postData.content_html,
|
||
pages: pagesData
|
||
}),
|
||
contentType: 'application/json',
|
||
success: function (result) {
|
||
if (result.code === 0) {
|
||
deferred.resolve(result.data);
|
||
} else {
|
||
deferred.reject(result.msg || '保存失败');
|
||
}
|
||
},
|
||
error: function (xhr) {
|
||
deferred.reject('网络错误: ' + xhr.statusText);
|
||
}
|
||
});
|
||
}).catch(function (err) {
|
||
deferred.reject(err);
|
||
});
|
||
|
||
return deferred.promise();
|
||
}
|
||
|
||
/**
|
||
* 将生成的canvas数组显示为缩略图
|
||
* @param {Array} canvases canvas对象数组
|
||
* @param {string} containerSelector 容器选择器
|
||
*/
|
||
function showGeneratedThumbnails(canvases, containerSelector) {
|
||
var $container = $(containerSelector);
|
||
$container.empty();
|
||
|
||
for (var i = 0; i < canvases.length; i++) {
|
||
var thumb = canvases[i].toDataURL('image/jpeg', 0.5);
|
||
var $item = $('<div class="phone-thumb-item" data-index="' + i + '">' +
|
||
'<img src="' + thumb + '" alt="第' + (i + 1) + '页">' +
|
||
'<span class="phone-thumb-page">' + (i + 1) + '</span>' +
|
||
'</div>');
|
||
$container.append($item);
|
||
}
|
||
}
|
||
|
||
// ===== 配置切换 =====
|
||
|
||
/**
|
||
* 切换尺寸
|
||
*/
|
||
function switchSize(name) {
|
||
config.size = name;
|
||
}
|
||
|
||
// ===== 逐页对齐 =====
|
||
|
||
/**
|
||
* 设置指定页的对齐方式
|
||
* @param {number} pageNum 页码(1-based)
|
||
* @param {string} align 'top'|'center'
|
||
*/
|
||
function setPageAlignment(pageNum, align) {
|
||
if (!config.pageAlignments) {
|
||
config.pageAlignments = {};
|
||
}
|
||
config.pageAlignments[pageNum] = align;
|
||
}
|
||
|
||
/**
|
||
* 导出内容流为长图
|
||
*/
|
||
function exportLongImage() {
|
||
var deferred = $.Deferred();
|
||
var $flow = $('#content-flow');
|
||
if (!$flow.length) {
|
||
deferred.reject('内容流容器不存在');
|
||
return deferred.promise();
|
||
}
|
||
|
||
// 临时隐藏交互元素
|
||
$flow.find('.break-inserter').hide();
|
||
$flow.find('.page-break-marker').hide();
|
||
|
||
// 获取内容流的实际高度
|
||
var flowWidth = $flow.outerWidth();
|
||
var flowHeight = $flow[0].scrollHeight;
|
||
|
||
html2canvas($flow[0], {
|
||
scale: 2,
|
||
useCORS: true,
|
||
backgroundColor: '#ffffff',
|
||
width: flowWidth,
|
||
height: flowHeight,
|
||
logging: false
|
||
}).then(function (canvas) {
|
||
// 恢复交互元素
|
||
$flow.find('.break-inserter').show();
|
||
$flow.find('.page-break-marker').show();
|
||
|
||
// 触发下载
|
||
var link = document.createElement('a');
|
||
link.download = 'phone-image-long-' + Date.now() + '.png';
|
||
link.href = canvas.toDataURL('image/png');
|
||
link.click();
|
||
|
||
deferred.resolve(canvas);
|
||
}).catch(function (err) {
|
||
// 恢复交互元素
|
||
$flow.find('.break-inserter').show();
|
||
$flow.find('.page-break-marker').show();
|
||
deferred.reject('长图导出失败: ' + err);
|
||
});
|
||
|
||
return deferred.promise();
|
||
}
|
||
|
||
// ===== 工具方法 =====
|
||
|
||
/**
|
||
* HTML转义
|
||
*/
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
var div = document.createElement('div');
|
||
div.appendChild(document.createTextNode(text));
|
||
return div.innerHTML;
|
||
}
|
||
|
||
/**
|
||
* 获取当前配置
|
||
*/
|
||
function getConfig() {
|
||
return $.extend({}, config);
|
||
}
|
||
|
||
/**
|
||
* 获取分页结果
|
||
*/
|
||
function getPages() {
|
||
return pages.slice();
|
||
}
|
||
|
||
// ===== 公开API =====
|
||
return {
|
||
init: init,
|
||
render: render,
|
||
generateImages: generateImages,
|
||
saveImages: saveImages,
|
||
showGeneratedThumbnails: showGeneratedThumbnails,
|
||
switchSize: switchSize,
|
||
getConfig: getConfig,
|
||
getPages: getPages,
|
||
setPageAlignment: setPageAlignment,
|
||
renderContentFlow: renderContentFlow,
|
||
insertPageBreak: insertPageBreak,
|
||
removePageBreak: removePageBreak,
|
||
exportLongImage: exportLongImage
|
||
};
|
||
})();
|