补充主题静态资源与归档模板
This commit is contained in:
1 parent
1268a55dd0
commit
3247db7ed1
265 files changed
+59174
No files matched your search
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* 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="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="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]' });
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', autoInit);
|
||||
} else {
|
||||
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;
|
||||
})();
|
||||
Reference in new issue
Block a user