// 部分代码来自ZIBILL主题作者 // canvas生成海报 const poster = (function() { const DEBUG = false const WIDTH = 700 const HEIGHT = 850 /** * 将图片URL加载为Image对象,并返回Promise * @param {string} url - 图片URL * @returns {Promise} - 加载完成的Image对象 */ function loadImage(url) { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = "Anonymous"; img.onload = () => resolve(img); img.onerror = () => reject(new Error('Failed to load image: ' + url)); img.src = url; }); } /** * 核心初始化函数 * @param {object} config - 配置对象 */ function init(config) { const $container = config.container || document.querySelector(config.selector) if (!$container) { if (config.callback) { config.callback(null) } return } // ------------------ 【修复点 2:清理容器,避免PJAX残余】 ------------------ // 在重新绘制前,确保清空容器,移除旧的 img 和 wrapper $container.innerHTML = ''; // ----------------------------------------------------------------------- $container.style.border = '1px solid #f0f0f0' const $wrapper = createDom('div', 'id', 'wrapper') const $canvas = createDom('canvas', 'id', 'canvas', 'block') const $day = createDom('canvas', 'id', 'day') const $date = createDom('canvas', 'id', 'date') const $title = createDom('canvas', 'id', 'title') const $content = createDom('canvas', 'id', 'content') const $logo = createDom('canvas', 'id', 'logo') const $siteName = createDom('canvas', 'id', 'siteName') const $description = createDom('canvas', 'id', 'description') const $customText1 = createDom('canvas', 'id', 'customText1') const $customText2 = createDom('canvas', 'id', 'customText2') appendChilds($wrapper, $canvas, $day, $siteName, $date, $title, $content, $logo, $description, $customText1, $customText2) $container.appendChild($wrapper) const date = new Date() // day canvas const dayStyle = { font: 'bold 80px Helvetica', color: 'rgba(255, 255, 255, 1)', position: 'right' } drawOneline($day, dayStyle, date.getDate()); // date canvas const dateStyle = { font: '23px Helvetica', color: 'rgba(255, 255, 255, 1)', position: 'right' } drawOneline($date, dateStyle, date.getFullYear() + '/' + (date.getMonth() + 1)) // title canvas const titleStyle = { font: '30px Helvetica', lineHeight: 1.5, color: 'rgba(255, 255, 255, 1)', length: 2, position: 'left' } titleStyle.font = (config.titleStyle && config.titleStyle.font) || titleStyle.font titleStyle.color = (config.titleStyle && config.titleStyle.color) || titleStyle.color titleStyle.position = (config.titleStyle && config.titleStyle.position) || titleStyle.position drawMoreLines($title, titleStyle, config.title) // site name canvas const siteNameStyle = { font: '40px Helvetica', lineHeight: 1.3, color: 'rgba(255, 255, 255, 0.95)', position: 'left' } siteNameStyle.font = (config.siteNameStyle && config.siteNameStyle.font) || siteNameStyle.font siteNameStyle.color = (config.siteNameStyle && config.siteNameStyle.color) || siteNameStyle.color siteNameStyle.lineHeight = (config.siteNameStyle && config.siteNameStyle.lineHeight) || siteNameStyle.lineHeight siteNameStyle.position = (config.siteNameStyle && config.siteNameStyle.position) || siteNameStyle.position drawMoreLines($siteName, siteNameStyle, config.siteName) // description canvas const descriptionStyle = { font: '25px Helvetica', color: 'rgba(236, 241, 255, 1)', lineHeight: 1.2, position: 'left' } descriptionStyle.font = (config.descriptionStyle && config.descriptionStyle.font) || descriptionStyle.font descriptionStyle.color = (config.descriptionStyle && config.descriptionStyle.color) || descriptionStyle.color descriptionStyle.lineHeight = (config.descriptionStyle && config.descriptionStyle.lineHeight) || descriptionStyle.lineHeight descriptionStyle.position = (config.descriptionStyle && config.descriptionStyle.position) || descriptionStyle.position drawMoreLines($description, descriptionStyle, config.description) // custom text 1 canvas const customText1Style = { font: '28px Helvetica', lineHeight: 1.3, position: 'left', color: 'rgba(0, 0, 0, 1)' } customText1Style.font = (config.customText1Style && config.customText1Style.font) || customText1Style.font customText1Style.color = (config.customText1Style && config.customText1Style.color) || customText1Style.color customText1Style.lineHeight = (config.customText1Style && config.customText1Style.lineHeight) || customText1Style.lineHeight customText1Style.position = (config.customText1Style && config.customText1Style.position) || customText1Style.position drawMoreLines($customText1, customText1Style, config.customText1 || '') // custom text 2 canvas const customText2Style = { font: '28px Helvetica', lineHeight: 1.3, position: 'left', color: 'rgba(0, 0, 0, 1)' } customText2Style.font = (config.customText2Style && config.customText2Style.font) || customText2Style.font customText2Style.color = (config.customText2Style && config.customText2Style.color) || customText2Style.color customText2Style.lineHeight = (config.customText2Style && config.customText2Style.lineHeight) || customText2Style.lineHeight customText2Style.position = (config.customText2Style && config.customText2Style.position) || customText2Style.position drawMoreLines($customText2, customText2Style, config.customText2 || '') // content canvas const contentStyle = { font: '22px Helvetica', lineHeight: 1.5, position: 'left', color: 'rgba(236, 241, 255, 1)' } contentStyle.font = (config.contentStyle && config.contentStyle.font) || contentStyle.font contentStyle.color = (config.contentStyle && config.contentStyle.color) || contentStyle.color contentStyle.lineHeight = (config.contentStyle && config.contentStyle.lineHeight) || contentStyle.lineHeight contentStyle.position = (config.contentStyle && config.contentStyle.position) || contentStyle.position drawMoreLines($content, contentStyle, config.content); // ------------------ 【修复点 1:使用 Promise.all 等待所有图片加载】 ------------------ Promise.all([ loadImage(config.banner), loadImage(config.logo), loadImage(config.qrcode) ]).then(([image, logo, qrcode]) => { $canvas.width = WIDTH; $canvas.height = HEIGHT; const ctx = $canvas.getContext('2d') // 1. 绘制背景 ctx.fillStyle = 'rgba(255, 255, 255, 1)'; ctx.fillRect(0, 0, $canvas.width, $canvas.height); // 2. 绘制 Banner const imgRect = coverImg($canvas.width - 40, $canvas.height / 1.2 - 40, image.width, image.height); ctx.drawImage(image, imgRect.sx, imgRect.sy, imgRect.sWidth, imgRect.sHeight, 20, 20, $canvas.width - 40, $canvas.height / 1.2 - 40); // 3. 绘制覆盖层 ctx.fillStyle="rgba(0, 0, 0, 0.6)"; ctx.fillRect(20, 20, $canvas.width - 40, $canvas.height / 1.2 - 40); // 4. 绘制时间 (使用预先绘制好的 Canvas) ctx.drawImage($day, -20, 50) ctx.drawImage($date, -21, 125) // 5. 绘制 Logo var logoh = 140; var cwidth = logoh * (logo.width / logo.height); ctx.drawImage(logo, 20, $canvas.height / 1.2 - 10, cwidth, logoh); // 6. 绘制二维码 ctx.drawImage(qrcode, $canvas.width - 150, $canvas.height / 1.2 + 10, 120, 120); // 7. 绘制文字 (使用预先绘制好的 Canvas) ctx.drawImage($siteName, 10, 60) ctx.drawImage($description, 10, 120) ctx.drawImage($customText1, 270, $canvas.height / 2 + 310) ctx.drawImage($customText2, 215, $canvas.height / 2 + 360) ctx.drawImage($title, 20, $canvas.height / 2 + 60) ctx.drawImage($content, 20, $canvas.height / 2 + 120) ctx.strokeStyle = 'rgba(122, 122, 122, 0.5)'; // 8. 转换并显示最终图片 const img = new Image(); img.crossOrigin = "Anonymous"; img.src = $canvas.toDataURL('image/png') const radio = config.radio || 0.7 img.width = WIDTH * radio img.height = HEIGHT * radio img.className = 'poster_load'; // 清理 Canvas 避免在屏幕上停留 ctx.clearRect(0, 0, $canvas.width, $canvas.height) $canvas.style.display = 'none' // 移除旧的 wrapper 并添加最终海报图 $container.appendChild(img); // 必须先移除再调用 callback,以确保 DOM 结构干净 $container.removeChild($wrapper) if (config.callback) { config.callback($container) } }).catch(error => { console.error('海报图片加载失败:', error); // 可在此处添加错误处理,例如显示一个错误信息 if ($wrapper.parentNode === $container) { $container.removeChild($wrapper); } if (config.callback) { // 即使失败也调用回调,传入 null 或错误信息,以便上层逻辑处理 config.callback(null); } }); // ----------------------------------------------------------------------- } // 裁剪函数(不变) function containImg(sx, sy, box_w, box_h, source_w, source_h) { var dx = sx, dy = sy, dWidth = box_w, dHeight = box_h; if (source_w > source_h || (source_w == source_h && box_w < box_h)) { dHeight = source_h * dWidth / source_w; dy = sy + (box_h - dHeight) / 2; } else if (source_w < source_h || (source_w == source_h && box_w > box_h)) { dWidth = source_w * dHeight / source_h; dx = sx + (box_w - dWidth) / 2; } return { dx, dy, dWidth, dHeight } } // 覆盖函数:按目标区域等比缩放并居中裁切,尽量不留白 function coverImg(box_w, box_h, source_w, source_h) { const scale = Math.max(box_w / source_w, box_h / source_h); const sWidth = box_w / scale; const sHeight = box_h / scale; const sx = (source_w - sWidth) / 2; const sy = (source_h - sHeight) / 2; return { sx, sy, sWidth, sHeight } } // 创建 DOM 元素(不变) function createDom(name, key, value, display = 'none') { const $dom = document.createElement(name) $dom.setAttribute(key, value) $dom.style.display = display $dom.width = WIDTH $dom.height = HEIGHT // 新增:确保 canvas 都有默认高度,尽管后续会重设 return $dom } // 追加子元素(不变) function appendChilds(parent, ...doms) { doms.forEach(dom => { parent.appendChild(dom) }) } // 绘制单行文本(不变) function drawOneline(canvas, style, content) { const ctx = canvas.getContext('2d') canvas.height = parseInt(style.font.match(/\d+/), 10) + 20 ctx.font = style.font ctx.fillStyle = style.color ctx.textBaseline = 'top' let lineWidth = 0 let idx = 0 let truncated = false for (let i = 0; i < content.length; i++) { lineWidth += ctx.measureText(content[i]).width; if (lineWidth > canvas.width - 60) { truncated = true idx = i break } } let padding = 30 if (truncated) { content = content.substring(0, idx) // 修正:如果内容被截断,应该重新计算截断后的文本宽度,再计算 padding lineWidth = ctx.measureText(content).width; if (style.position === 'center') { padding = canvas.width / 2 - lineWidth / 2; } else if (style.position === 'left') { padding = 30; // 保持左侧间距 } else { padding = canvas.width - 30; // 保持右侧间距 } } if (DEBUG) { ctx.strokeStyle = "#6fda92"; ctx.strokeRect(0, 0, canvas.width, canvas.height); } if (style.position === 'center') { ctx.textAlign = 'center'; ctx.fillText(content, canvas.width / 2, 0) } else if (style.position === 'left') { ctx.textAlign = 'left' ctx.fillText(content, padding, 0) } else { ctx.textAlign = 'right' ctx.fillText(content, canvas.width - padding, 0) } } // 绘制多行文本(不变) function drawMoreLines(canvas, style, content) { const ctx = canvas.getContext('2d') const fontHeight = parseInt(style.font.match(/\d+/), 10) let totalLines = 0; // 预计算行高并设置 Canvas 高度 let lineWidth = 0 let currentLine = 0 for (let i = 0; i < content.length; i++) { lineWidth += ctx.measureText(content[i]).width; if (lineWidth > canvas.width - 120) { currentLine++; lineWidth = 0 } if (i === content.length - 1) { currentLine++; } } totalLines = currentLine; canvas.height = (fontHeight * style.lineHeight) * totalLines + 20; // 留出一些底部边距 if (DEBUG) { ctx.strokeStyle = "#6fda92"; ctx.strokeRect(0, 0, canvas.width, canvas.height); } ctx.font = style.font ctx.fillStyle = style.color ctx.textBaseline = 'top' ctx.textAlign = 'center' let alignX = 0 if (style.position === 'center') { alignX = canvas.width / 2; } else if (style.position === 'left') { ctx.textAlign = 'left' alignX = 40 } else { ctx.textAlign = 'right' alignX = canvas.width - 40 } lineWidth = 0 let lastSubStrIndex = 0 let offsetY = 0 for (let i = 0; i < content.length; i++) { lineWidth += ctx.measureText(content[i]).width; if (lineWidth > canvas.width - 120) { ctx.fillText(content.substring(lastSubStrIndex, i), alignX, offsetY); offsetY += fontHeight * style.lineHeight lineWidth = 0 lastSubStrIndex = i } if (i === content.length - 1) { ctx.fillText(content.substring(lastSubStrIndex, i + 1), alignX, offsetY); } } } return { init } })() // ajax生成文章海报 // 此处调用部分需要确保在 PJAX 环境下,该事件监听器能够重新绑定(如果使用传统的 document.ready,在 PJAX 中需要改为监听 pjax:success 等事件) $('body').on('click', '.cr_poster', function () { let closeLoading = null; closeLoading = cocoMessage.loading('海报生成中...'); var $trigger = $(this); var post_id = $(this).attr('poster-data'); var banner = $(this).attr('banner'); var single_content = $('.single-content'); var t_content = $('#post-'+post_id+' .t_content'); var modal = '#share_modal_' + post_id; var $modalList = $(modal); var $modal = $trigger.closest(modal); if ($modal.length === 0) { $modal = $(modal + ':visible').first(); } if ($modal.length === 0) { $modal = $modalList.first(); } if ($modal.length === 0) { return; } // 确保 content 区域存在 var content_element = single_content.length > 0 ? single_content[0] : (t_content.length > 0 ? t_content[0] : null); if (!content_element) { // 如果没有内容元素,则不执行后续操作或给出提示 console.error('无法获取文章内容元素。'); return; } var content = content_element.innerText; var content_length = content.length; var permalink = t_content.length > 0 ? window.location.origin + '/moments/' + post_id : window.location.origin + window.location.pathname; var title = t_content.length > 0 ? '瞬间' : (document.title || '').split(' - ')[0]; var siteName = Theme.site_title; // 确保 QRious 库可用 if (typeof QRious === 'undefined') { console.error('QRious 库未加载。'); return; } // 检查 #twoCode 是否存在,如果不存在需要动态创建,否则 QRious 会失败 let twoCodeCanvas = document.getElementById("twoCode"); if (!twoCodeCanvas) { twoCodeCanvas = document.createElement('canvas'); twoCodeCanvas.id = 'twoCode'; twoCodeCanvas.style.display = 'none'; document.body.appendChild(twoCodeCanvas); // 插入到 DOM 中 } new QRious({ element: twoCodeCanvas, value: permalink, size: 260, }); var $posterBoxAp = $modal.find('.poster_box_ap').first(); if ($posterBoxAp.length === 0) { return; } // ------------------ 【修复点 2:清理容器】 ------------------ // 仅在当前弹窗作用域内移除旧容器,避免影响 PJAX 残留节点 $posterBoxAp.find('.poster_box').remove(); // 重新创建并插入新的 poster_box var $posterBox = $('
'); $posterBoxAp.append($posterBox); // ----------------------------------------------------------------------- if (content_length > 120) { content = content.substring(0, 82) + '...' } // 海报生成回调 function Posterdown(e) { // 检查海报是否成功生成 if (e && $posterBox.find('img').length > 0) { closeLoading?.() cocoMessage.success("海报生成成功!") // 图片加载完成后才触发 var url = $posterBox.find('img').attr('src'); var $shareBox = $modal.find('.post_share_box'); $shareBox.removeClass('hide'); $modal.find('.poster_download').attr('href', url).attr('download', 'poster_' + post_id + '.png'); } else { // 错误处理,如果海报生成失败 closeLoading?. (); cocoMessage.error("海报生成失败,请重试!") } } poster.init({ banner: banner, selector: modal + ' .poster_box', container: $posterBox[0], title: title, siteName: siteName, description: Theme.admin_des, content: content, logo: Theme.site_logo, qrcode: $('#twoCode').attr('src'), customText1: '扫描右侧二维码', customText2: '或访问:https://anian.net', customText1Style: { font: '24px Helvetica', color: 'rgba(0, 0, 0, 1)' }, customText2Style: { font: '24px Helvetica', color: 'rgba(0, 0, 0, 1)' }, callback: Posterdown }); }); $(document).on('pjax:success', function (event) { // 清理 PJAX 残留的 modal:只删除那些在当前页面没有对应按钮的 modal var allModals = $('[id^="share_modal_"]'); var currentPostIds = {}; // 收集当前页面所有的 post_id $('.cr_poster').each(function() { var pid = $(this).attr('poster-data'); if (pid) { currentPostIds[pid] = true; } }); // 只删除残留的 modal(不在当前页面按钮列表中的) allModals.each(function() { var $m = $(this); var modalId = $m.attr('id'); var postId = modalId ? modalId.replace('share_modal_', '') : null; if (postId && !currentPostIds[postId]) { $m.remove(); } }); }); function convertImgToBase64(url, callback) { var canvas = document.createElement('canvas'); var img = new Image(); img.crossOrigin = 'Anonymous'; img.onload = function () { canvas.width = img.width; canvas.height = img.height; var ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); var ext = img.src.substring(img.src.lastIndexOf('.') + 1).toLowerCase(); // 修正:确保 toDataURL 使用正确的 MIME type,如果是 jpg/jpeg 应使用 image/jpeg var mimeType = (ext === 'jpg' || ext === 'jpeg') ? 'image/jpeg' : 'image/png'; var dataURL = canvas.toDataURL(mimeType); callback(dataURL); canvas = null; }; img.onerror = function() { console.error('convertImgToBase64: 无法加载图片 ' + url); callback(null); } img.src = url; } function handleShare(e, pic, title, pathname) { var permalink = window.location.origin + pathname; // 确保 cocoMessage 库可用 if (typeof cocoMessage === 'undefined') { console.error('cocoMessage 库未加载。'); // 如果 cocoMessage 不可用,可以替换为原生的 alert if (e === 'copy') { alert("链接已复制到剪贴板!"); } } switch (e) { case 'wb': var url = `https://service.weibo.com/share/share.php?url=${encodeURIComponent(permalink)}&type=button&language=zh_cn&pic=${encodeURIComponent(pic)}&title=${encodeURIComponent(title)}` window.open(url); break case 'qzone': var url = `https://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url=${encodeURIComponent(permalink)}&title=${encodeURIComponent(title)}&pics=${encodeURIComponent(pic)}` window.open(url); break case "qq": var url = `http://connect.qq.com/widget/shareqq/index.html?url=${encodeURIComponent(permalink)}&title=${encodeURIComponent(title)}&pics=${encodeURIComponent(pic)}` window.open(url); break; case "copy": if (navigator.clipboard) { navigator.clipboard.writeText(permalink).then(function() { cocoMessage.success("链接已复制到剪贴板!"); }, function(err) { cocoMessage.error("复制失败,请手动复制链接"); }); } else { // 兼容旧浏览器 var tempInput = document.createElement('input'); tempInput.value = permalink; document.body.appendChild(tempInput); tempInput.select(); try { document.execCommand('copy'); cocoMessage.success("链接已复制到剪贴板!"); } catch (err) { cocoMessage.error("复制失败,请手动复制链接"); } document.body.removeChild(tempInput); } break; default: break } }