/** * 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; document.querySelectorAll(this.options.triggerSelector).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 = ' 保存图片'; 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 = '