From b53ba68f6851fe62541485b88d023ee4d8f27318 Mon Sep 17 00:00:00 2001 From: augushong Date: Sat, 2 May 2026 09:24:31 +0800 Subject: [PATCH] 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 --- public/static/js/phone-image.js | 201 +++++++++++++++++++++++++++++++- 1 file changed, 200 insertions(+), 1 deletion(-) diff --git a/public/static/js/phone-image.js b/public/static/js/phone-image.js index e0cd02f..a894ba7 100644 --- a/public/static/js/phone-image.js +++ b/public/static/js/phone-image.js @@ -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 = $(''); + + $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 = $('
' + + '-- 分页标记 --' + + '' + + '
'); + $flow.append($marker); + continue; + } + + // 内容块 + var $block = $('
' + block.html + '
'); + $flow.append($block); + + // 块与块之间的分页插入区域(不在最后一个块之后添加) + if (i < blocks.length - 1) { + var $inserter = $('
' + + '' + + '
'); + $flow.append($inserter); + } + } + } + + /** + * 在指定位置插入分页标记 + * 修改 postData.content_html,在对应位置插入
+ * @param {number} blockIndex - 在哪个块之后插入 (对应 break-inserter 的 data-after-index) + */ + function insertPageBreak(blockIndex) { + var cleanHtml = preprocessContent(postData.content_html); + var $temp = $('
').html(cleanHtml); + var children = $temp.children(); + + // 在 blockIndex 位置的元素之后插入
+ if (blockIndex >= 0 && blockIndex < children.length) { + $(children[blockIndex]).after('
'); + } else { + $temp.append('
'); + } + + postData.content_html = $temp.html(); + + // 重新渲染 + render(); + } + + /** + * 删除指定位置的分页标记 + * @param {number} blockIndex - 分页标记的 data-index + */ + function removePageBreak(blockIndex) { + // 统计这是第几个 page-break + var cleanHtml = preprocessContent(postData.content_html); + var $temp = $('
').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 }; })();