Files
wordpress-theme-aksrt/assets/js/poster-share.js
T
kate522 5fd7c212ed fix(runtime): 按参考主题硬切换为 loader/main/auto_fun 单一 DOM 生命周期
- 删除 AksrtFrontend registry/rehydrate 双路架构及 build/public.js 拼包入口,
  前台唯一初始路径为 PHP 本地栈(jQuery 1.9.1 → Bootstrap 3.4.1)
  → loader_js(_win/aksrtData 唯一注入)→ tbquire(['main']) → runtime/main.min.js
- window.auto_fun = debounce(z_auto_fun, 100) 为唯一重扫入口;所有模块改为
  执行一次 + off/on('auto_fun.aksrt-*') 命名空间重扫,data/class 标记幂等
- 局部更新协议统一:HTML 注入 → post_ajax.ed → auto_fun();
  远程 modal → loaded.bs.modal → auto_fun();JSON 成功 → zib_ajax.success
- 灯箱恢复 Miniimgbox/imgbox 唯一 owner(imgbox-container、a[data-imgbox]、
  imgbox_data),删除 AksrtLightbox/data-aksrt-lightbox 第二实现及 CSS/overlay,
  _win.imgbox* 重新接上 post-display 设置
- 死代码清理:runtime 下 11 组被自有实现替代的旧模块副本、无消费 vendored 库、
  顶层 grade.min.js、revenue.js、无调用方的 aksrt_get_win_runtime_config(),
  以及 loader 中 16 条死 mapping
- PHP 入队改为本地栈 + loader_js,routed/widget/submit 依赖统一换绑;
  用户卡片发布按钮恢复 nav_user_newposts_btn 设置门控
- 测试对齐:新增 frontend-lifecycle-contract,重写 navigation/lazyload/toast/
  auth-source/comment/page-display/post-visual 等断言为目标架构契约

验证:node --test widget-system+navigation 223 通过 0 失败;
构建脚本重建 main/imgbox min 与 CSS bundles;改动 PHP php -l、JS node --check
全部通过;git diff --check 干净;credits 重入 harness 通过。
2026-09-03 12:44:31 +08:00

407 lines
12 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;
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 = '<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() {
document.querySelectorAll('[data-aksrt-poster]').forEach(function (el) {
if (el.dataset.posterInit) return;
el.dataset.posterInit = '1';
var config = {};
try {
config = JSON.parse(el.dataset.aksrtPoster || '{}');
} catch (e) {}
new PosterShare({ triggerSelector: '[data-aksrt-poster]' });
});
}
jQuery(document).off('auto_fun.aksrt-poster-share').on('auto_fun.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;
})();