Files
wordpress-theme-aksrt/assets/js/poster-share.js
T
kate522 1f4a39d8f8 feat(runtime): 全站前端脚本统一接入 AksrtFrontend 全局 DOM 生命周期
- rehydrate.js 新增 boot()/aksrt:ready 事件,支持 boot 后晚注册立即以 init 触发
- aksrt-runtime.js 初始加载改走 AksrtFrontend.boot,tooltip/倒计时注册为组件
- 约 30 个前端脚本(首页、评论、支付、路由、页面叶子、widgets)
  从直接 DOMContentLoaded 迁移为 AksrtFrontend.register,并加幂等标记
- section-navs 依赖改为 aksrt-runtime,保证注册时序
- 清理死代码:删除孤儿 loader.js/main.js 及 runtime/page-navs 遗留副本
- loader.js 移除已迁移模块的 tbquire 路径映射,main.js 相应瘦身
- build-public-assets.mjs 增加 runtime/main.min.js 自动压缩步骤
- 新增 4 个契约测试(homepage-dom/rehydrate-runtime/global-dom-init/
  public-bundle-parity),3 个既有 harness 适配注册表初始化方式
2026-09-02 21:48:56 +08:00

411 lines
13 KiB
JavaScript

/**
* AKSRT Poster Share — 海报分享
*
* 生成文章分享海报图片,支持下载和分享。
* 使用 Canvas API 绘制,无需外部依赖。
*
* @package AKSRT
*/
(function () {
'use strict';
/**
* 初始化海报分享
*
* @param {Object} options 配置项
*/
function PosterShare(options) {
this.options = Object.assign({
triggerSelector: '[data-aksrt-poster]',
title: '',
excerpt: '',
author: '',
date: '',
siteName: '',
qrCodeUrl: '',
width: 750,
height: 1334,
backgroundColor: '#ffffff',
textColor: '#333333',
accentColor: '#1890ff',
}, options || {});
this.init();
}
PosterShare.prototype.init = function () {
var self = this;
var scope = this.options.root || document;
var triggers = [];
if (scope.nodeType === 1 && scope.matches(this.options.triggerSelector)) triggers.push(scope);
Array.prototype.push.apply(triggers, scope.querySelectorAll(this.options.triggerSelector));
triggers.forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.preventDefault();
// Unified payload first (title/excerpt/author/date/qr), then allow an
// explicit data-aksrt-poster JSON blob to override individual fields.
var resolved = (window.AksrtShareData && typeof window.AksrtShareData.resolve === 'function')
? window.AksrtShareData.resolve(btn)
: {};
var config = {};
try {
config = JSON.parse(btn.dataset.aksrtPoster || '{}');
} catch (err) {}
var merged = Object.assign({}, self.options, {
title: resolved.title || self.options.title,
excerpt: resolved.desc || self.options.excerpt,
author: resolved.author || self.options.author,
date: resolved.date || self.options.date,
siteName: resolved.siteName || self.options.siteName,
qrCodeUrl: resolved.qrcode || self.options.qrCodeUrl
}, config);
self.generate(merged);
});
});
};
var WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
var FONT_FAMILY = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
PosterShare.prototype.generate = function (config) {
var self = this;
config.width = config.width || 768;
config.height = config.height || 1168;
var overlay = this.createOverlay();
document.body.appendChild(overlay);
var canvas = document.createElement('canvas');
canvas.width = config.width;
canvas.height = config.height;
var ctx = canvas.getContext('2d');
// Background
ctx.fillStyle = config.backgroundColor;
ctx.fillRect(0, 0, config.width, config.height);
var coverSources = [config.coverImg, config.banner, config.banner_default, config.banner_spare].filter(Boolean);
if (coverSources.length) {
self.loadImageWithRetry(coverSources, 0, function (coverImg) {
self.drawPosterContent(ctx, canvas, config, overlay, coverImg);
}, function () {
self.drawPosterContent(ctx, canvas, config, overlay, null);
});
} else {
self.drawPosterContent(ctx, canvas, config, overlay, null);
}
};
PosterShare.prototype.loadImageWithRetry = function (sources, idx, onLoad, onFail) {
if (idx >= sources.length) { onFail(); return; }
var self = this;
var retries = 0;
var src = sources[idx];
function tryLoad() {
var img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function () { onLoad(img); };
img.onerror = function () {
retries++;
if (retries < 3) { tryLoad(); }
else { self.loadImageWithRetry(sources, idx + 1, onLoad, onFail); }
};
img.src = src;
}
tryLoad();
};
PosterShare.prototype.drawPosterContent = function (ctx, canvas, config, overlay, coverImg) {
var self = this;
var W = config.width;
var isProduct = config.isProduct;
var y = 0;
// Cover image area (top)
var coverH = isProduct ? W : Math.round(W * 0.52);
if (coverImg) {
var imgRatio = coverImg.width / coverImg.height;
var drawW, drawH, drawX, drawY;
if (imgRatio > W / coverH) {
drawH = coverH; drawW = coverH * imgRatio;
drawX = -(drawW - W) / 2; drawY = 0;
} else {
drawW = W; drawH = W / imgRatio;
drawX = 0; drawY = -(drawH - coverH) / 2;
}
ctx.drawImage(coverImg, drawX, drawY, drawW, drawH);
} else {
ctx.fillStyle = config.accentColor;
ctx.fillRect(0, 0, W, coverH);
}
// Date/weekday badge (top-left)
if (config.date) {
var d = new Date(config.date);
if (!isNaN(d.getTime())) {
var weekday = WEEKDAYS[d.getDay()];
var dateStr = (d.getMonth() + 1) + '/' + d.getDate();
ctx.fillStyle = 'rgba(0,0,0,0.55)';
ctx.beginPath();
ctx.roundRect ? ctx.roundRect(20, 20, 80, 65, 8) : ctx.rect(20, 20, 80, 65);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 20px ' + FONT_FAMILY;
ctx.textAlign = 'center';
ctx.fillText(weekday, 60, 47);
ctx.font = '16px ' + FONT_FAMILY;
ctx.fillText(dateStr, 60, 72);
ctx.textAlign = 'left';
}
}
y = coverH + 36;
// Title with vertical accent line
ctx.fillStyle = config.accentColor;
ctx.fillRect(50, y - 6, 4, 44);
ctx.fillStyle = config.textColor;
ctx.font = 'bold 32px ' + FONT_FAMILY;
var titleLines = this.wrapText(ctx, config.title, W - 140);
titleLines.slice(0, 3).forEach(function (line) {
ctx.fillText(line, 66, y + 28);
y += 44;
});
y += 16;
// Product price section
if (isProduct && config.price !== undefined) {
ctx.font = 'bold 36px ' + FONT_FAMILY;
ctx.fillStyle = '#e74c3c';
var priceLabel = config.priceUnit === 'points' ? (config.price + ' 积分') : ('¥' + config.price);
ctx.fillText(priceLabel, 60, y + 10);
if (config.originalPrice && config.originalPrice > config.price) {
var priceW = ctx.measureText(priceLabel).width;
ctx.font = '22px ' + FONT_FAMILY;
ctx.fillStyle = '#999';
var origText = '¥' + config.originalPrice;
ctx.fillText(origText, 60 + priceW + 16, y + 10);
var origW = ctx.measureText(origText).width;
ctx.strokeStyle = '#999';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(60 + priceW + 16, y + 5);
ctx.lineTo(60 + priceW + 16 + origW, y + 5);
ctx.stroke();
}
if (config.priceTag) {
ctx.font = '18px ' + FONT_FAMILY;
ctx.fillStyle = '#fff';
var tagW = ctx.measureText(config.priceTag).width + 16;
ctx.fillStyle = config.accentColor;
ctx.beginPath();
ctx.roundRect ? ctx.roundRect(W - 60 - tagW, y - 10, tagW, 30, 4) : ctx.rect(W - 60 - tagW, y - 10, tagW, 30);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.fillText(config.priceTag, W - 60 - tagW / 2, y + 10);
ctx.textAlign = 'left';
}
y += 48;
}
// Excerpt
if (config.excerpt) {
ctx.fillStyle = '#666';
ctx.font = '22px ' + FONT_FAMILY;
var excerptLines = this.wrapText(ctx, config.excerpt, W - 120);
excerptLines.slice(0, 5).forEach(function (line) {
ctx.fillText(line, 60, y + 4);
y += 34;
});
if (excerptLines.length > 5) {
ctx.fillText('...', 60, y + 4);
y += 34;
}
}
y += 20;
// Author & date
ctx.fillStyle = '#999';
ctx.font = '18px ' + FONT_FAMILY;
if (config.author) { ctx.fillText(config.author, 60, y); y += 26; }
// Bottom: site name + QR code
var bottomY = config.height - 110;
ctx.strokeStyle = '#eee';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(50, bottomY);
ctx.lineTo(W - 50, bottomY);
ctx.stroke();
if (config.siteName) {
ctx.fillStyle = '#999';
ctx.font = '18px ' + FONT_FAMILY;
ctx.fillText(config.siteName, 60, bottomY + 36);
}
if (config.qrCodeUrl) {
var qrImg = new Image();
qrImg.crossOrigin = 'anonymous';
qrImg.onload = function () {
var qrSize = 90;
var qrX = W - 50 - qrSize;
var qrY = bottomY + 6;
ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize);
ctx.fillStyle = '#999';
ctx.font = '13px ' + FONT_FAMILY;
ctx.textAlign = 'center';
ctx.fillText('扫码阅读', qrX + qrSize / 2, qrY + qrSize + 16);
ctx.textAlign = 'left';
self.showPoster(canvas, overlay);
};
qrImg.onerror = function () { self.showPoster(canvas, overlay); };
qrImg.src = config.qrCodeUrl;
} else {
self.showPoster(canvas, overlay);
}
};
PosterShare.prototype.showPoster = function (canvas, overlay) {
// 创建海报容器
var posterWrap = document.createElement('div');
posterWrap.className = 'aksrt-poster-share__wrap';
var img = document.createElement('img');
img.src = canvas.toDataURL('image/png');
img.className = 'aksrt-poster-share__image';
img.alt = '分享海报';
posterWrap.appendChild(img);
// 操作按钮
var actions = document.createElement('div');
actions.className = 'aksrt-poster-share__actions';
var downloadBtn = document.createElement('button');
downloadBtn.type = 'button';
downloadBtn.className = 'aksrt-poster-share__btn';
downloadBtn.innerHTML = '<i class="fa fas fa-download"></i> 保存图片';
downloadBtn.addEventListener('click', function () {
var link = document.createElement('a');
var postId = (window.aksrtData && aksrtData.postId) || Date.now();
link.download = 'poster_' + postId + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
});
actions.appendChild(downloadBtn);
var closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'aksrt-poster-share__btn aksrt-poster-share__btn--secondary';
closeBtn.textContent = '关闭';
closeBtn.addEventListener('click', function () {
overlay.remove();
});
actions.appendChild(closeBtn);
posterWrap.appendChild(actions);
// 替换加载状态
overlay.innerHTML = '';
overlay.appendChild(posterWrap);
};
PosterShare.prototype.createOverlay = function () {
var overlay = document.createElement('div');
overlay.className = 'aksrt-poster-share__overlay';
overlay.innerHTML = '<div class="aksrt-poster-share__loading"><i class="fa fas fa-spinner fa-spin"></i> 生成海报中...</div>';
overlay.addEventListener('click', function (e) {
if (e.target === overlay) overlay.remove();
});
return overlay;
};
PosterShare.prototype.wrapText = function (ctx, text, maxWidth) {
var lines = [];
var currentLine = '';
for (var i = 0; i < text.length; i++) {
var char = text[i];
var testLine = currentLine + char;
var metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && currentLine !== '') {
lines.push(currentLine);
currentLine = char;
} else {
currentLine = testLine;
}
}
if (currentLine) {
lines.push(currentLine);
}
return lines;
};
// 自动初始化
function autoInit(root) {
var triggers = [];
if (root && root.nodeType === 1 && root.matches('[data-aksrt-poster]')) triggers.push(root);
Array.prototype.push.apply(triggers, (root || document).querySelectorAll('[data-aksrt-poster]'));
if (!triggers.length) return;
var pending = triggers.filter(function (el) { return el.dataset.posterInit !== '1'; });
if (!pending.length) return;
var instance = new PosterShare({ root: root || document, triggerSelector: '[data-aksrt-poster]' });
pending.forEach(function (el) {
el.dataset.posterInit = '1';
el._aksrtPosterShare = instance;
});
}
window.AksrtFrontend.register('aksrt-poster-share', autoInit);
/**
* Generate a poster directly from a unified share payload, without binding
* to DOM triggers. Used by post-share.js for the "生成海报" share item.
*
* @param {Object} payload Canonical share payload (AksrtShareData shape).
*/
PosterShare.open = function (payload) {
payload = payload || {};
var inst = Object.create(PosterShare.prototype);
inst.options = {
width: 750,
height: 1334,
backgroundColor: '#ffffff',
textColor: '#333333',
accentColor: '#1890ff'
};
inst.generate(Object.assign({}, inst.options, {
title: payload.title || document.title,
excerpt: payload.excerpt || payload.desc || '',
author: payload.author || '',
date: payload.date || '',
siteName: payload.siteName || '',
qrCodeUrl: payload.qrCodeUrl || payload.qrcode || ''
}));
};
window.AksrtPosterShare = PosterShare;
})();