Files
halo-theme-pix/templates/assets/js/poster.js
T
2026-03-10 17:21:05 +08:00

520 lines
19 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 部分代码来自ZIBILL主题作者
// canvas生成海报
const poster = (function() {
const DEBUG = false
const WIDTH = 700
const HEIGHT = 1160
/**
* 将图片URL加载为Image对象,并返回Promise
* @param {string} url - 图片URL
* @returns {Promise<Image>} - 加载完成的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 = document.querySelector(config.selector)
// ------------------ 【修复点 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 $description = createDom('canvas', 'id', 'description')
appendChilds($wrapper, $canvas, $day, $date, $title, $content, $logo, $description)
$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)
// 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);
// description canvas
const descriptionStyle = {
font: '24px Helvetica',
color: 'rgba(180, 180, 180, 1)',
lineHeight: 1.2,
position: 'left'
}
drawMoreLines($description, descriptionStyle, config.description)
// ------------------ 【修复点 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.3)";
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 = 60;
var cwidth = logoh * (logo.width / logo.height);
ctx.drawImage(logo, 60, $canvas.height / 1.2 + 30, cwidth, logoh);
// 6. 绘制二维码
ctx.drawImage(qrcode, $canvas.width - 160, $canvas.height / 1.2 + 20, 120, 120);
// 7. 绘制文字 (使用预先绘制好的 Canvas)
ctx.drawImage($title, 20, $canvas.height / 2 + 20)
ctx.drawImage($content, 20, $canvas.height / 2 + 120)
ctx.drawImage($description, 20, $canvas.height / 1.2 + 110)
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);
// 可在此处添加错误处理,例如显示一个错误信息
$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) {
var sx = 0,
sy = 0,
sWidth = source_w,
sHeight = source_h;
if (source_w > source_h || (source_w == source_h && box_w < box_h)) {
sWidth = box_w * sHeight / box_h;
sx = (source_w - sWidth) / 2;
} else if (source_w < source_h || (source_w == source_h && box_w > box_h)) {
sHeight = box_h * sWidth / box_w;
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 () {
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');
// 确保 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 ? Theme.site_title + '-瞬间' : document.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 poster_box_selector = '#share_modal_' + post_id + ' .poster_box';
// ------------------ 【修复点 2:清理容器】 ------------------
// 先移除旧的 poster_box
$('.poster_box').remove();
// 重新创建并插入新的 poster_box
$('#share_modal_' + post_id + ' .poster_box_ap').append('<div class="poster_box"></div>');
// -----------------------------------------------------------------------
if (content_length > 120) {
content = content.substring(0, 80) + '...'
}
// Posterdown 函数中不再需要移除 loading_box,因为 init 会清空容器
function Posterdown(e) {
var modal = '#share_modal_' + post_id;
// 检查海报是否成功生成
if (e && $(modal + ' .poster_box img').length > 0) {
var url = $(modal + ' .poster_box img').attr('src');
$(modal + ' .post_share_box').removeClass('hide');
$(modal + ' .poster_download').attr('href', url).attr('download', 'poster_' + post_id + '.png');
} else {
// 错误处理,如果海报生成失败
cocoMessage.error("海报生成失败,请检查图片链接!");
}
}
// 移除 loading box 的时机提前,或者在 init 开始前完成
// $('.loading_box').remove(); // 原始代码中这行在创建 loading_box 之后,但其作用是移除**所有** .loading_box,可能存在问题
// 重新添加 loading 状态,并在 init 中等待
$(poster_box_selector).append('<div class="loading_box"><div uk-spinner></div></div>');
poster.init({
banner: banner,
selector: poster_box_selector, // 传入完整的选择器
title: title,
content: content,
logo: Theme.site_logo,
qrcode: $('#twoCode').attr('src'),
description: Theme.admin_des,
callback: Posterdown
});
// 移除 loading 动画
$(poster_box_selector + ' .loading_box').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
}
}