feat(phone-image): add content flow, interactive breaks, per-page alignment and long image export

T9: renderContentFlow(), insertPageBreak(), removePageBreak(), renderAlignmentToggles(), exportLongImage()

- Content flow renders blocks to #content-flow with interactive break-inserters

- Page break markers have delete buttons

- Per-page alignment toggle buttons on each content page

- Long image export hides interactive elements before html2canvas capture

- Event delegation with proper unbind/rebind to avoid duplicates
This commit is contained in:
augushong
2026-05-02 09:24:31 +08:00
parent bcd00e32ea
commit b53ba68f68

View File

@@ -58,6 +58,30 @@ var PhoneImageEngine = (function () {
if (userConfig) {
$.extend(config, userConfig);
}
// 内容流事件委托(只绑定一次)
$(document).off('click.phoneImage', '.break-inserter-btn');
$(document).off('click.phoneImage', '.remove-break-btn');
$(document).off('click.phoneImage', '.page-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', '.page-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();
});
}
/**
@@ -99,6 +123,12 @@ var PhoneImageEngine = (function () {
// 渲染到DOM
renderToDOM(sizeConfig);
// 渲染内容流(中间栏)
renderContentFlow(blocks);
// 渲染逐页对齐指示器
renderAlignmentToggles();
return pages;
}
@@ -588,6 +618,122 @@ var PhoneImageEngine = (function () {
}
}
/**
* 为每页渲染逐页对齐切换指示器
*/
function renderAlignmentToggles() {
var $pages = $('#paginated-preview .phone-image-page');
$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);
});
}
/**
* 渲染内容流到 #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="删除分页">&times;</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();
}
}
// ===== 图片生成 =====
/**
@@ -723,6 +869,54 @@ var PhoneImageEngine = (function () {
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();
}
// ===== 工具方法 =====
/**
@@ -759,6 +953,11 @@ var PhoneImageEngine = (function () {
switchSize: switchSize,
getConfig: getConfig,
getPages: getPages,
setPageAlignment: setPageAlignment
setPageAlignment: setPageAlignment,
renderContentFlow: renderContentFlow,
insertPageBreak: insertPageBreak,
removePageBreak: removePageBreak,
renderAlignmentToggles: renderAlignmentToggles,
exportLongImage: exportLongImage
};
})();