520 lines
19 KiB
JavaScript
520 lines
19 KiB
JavaScript
// 部分代码来自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
|
||
}
|
||
|
||
} |