feat(share): align social sharing runtime

This commit is contained in:
kate522 committed 2026-09-27 13:49:32 +08:00
1 parent 26ac8167f3
commit cf6c11852e
33 files changed
+802 -1919

No files matched your search

-305
View File
@@ -1,305 +0,0 @@
/**
* AKSRT Post Share
*
* Handles share dropdowns, social share targets, copy link,
* and WeChat bridge handoff with QR fallback.
*
* @package AKSRT
*/
(function () {
'use strict';
function getMetaContent(selectors) {
for (var i = 0; i < selectors.length; i += 1) {
var node = document.querySelector(selectors[i]);
var content = node ? node.getAttribute('content') : '';
if (content) return content;
}
return '';
}
function getPlatform(node) {
if (!node) return '';
return node.getAttribute('data-share-platform') || node.getAttribute('data-platform') || '';
}
function resolvePayload(node) {
// Unified payload via the shared builder; fall back to local resolution
// only if the builder script is unavailable.
if (window.AksrtShareData && typeof window.AksrtShareData.resolve === 'function') {
return window.AksrtShareData.resolve(node);
}
var dropdown = document.getElementById('share-dropdown');
var source = node && typeof node.closest === 'function'
? node.closest('[data-share-url], [data-share-title], [data-share-desc], [data-share-image]')
: null;
var context = source || dropdown;
function readAttr(name) {
return (node && node.getAttribute(name)) || (context && context.getAttribute(name)) || '';
}
return {
title: readAttr('data-share-title') || document.title,
url: readAttr('data-share-url') || window.location.href,
desc: readAttr('data-share-desc') || getMetaContent(['meta[property="og:description"]', 'meta[name="description"]']),
text: readAttr('data-share-text') || '',
image: readAttr('data-share-image') || getMetaContent(['meta[property="og:image"]']),
qrcode: readAttr('data-share-qrcode') || ''
};
}
function openWindow(url) {
if (!url) return;
window.open(url, '_blank', 'width=640,height=560,scrollbars=yes,resizable=yes');
}
function buildShareUrl(platform, payload) {
switch (platform) {
case 'weibo':
return 'https://service.weibo.com/share/share.php?url=' + encodeURIComponent(payload.url) + '&title=' + encodeURIComponent(payload.title + (payload.text ? ' ' + payload.text : ''));
case 'qq':
return 'https://connect.qq.com/widget/shareqq/index.html?url=' + encodeURIComponent(payload.url) + '&title=' + encodeURIComponent(payload.title) + '&desc=' + encodeURIComponent(payload.desc || payload.text || '');
case 'twitter':
return 'https://x.com/intent/tweet?url=' + encodeURIComponent(payload.url) + '&text=' + encodeURIComponent(payload.title);
case 'telegram':
return 'https://t.me/share/url?url=' + encodeURIComponent(payload.url) + '&text=' + encodeURIComponent(payload.title);
case 'linkedin':
return 'https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent(payload.url);
default:
return '';
}
}
function fallbackCopy(text) {
var textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0;';
textarea.setAttribute('readonly', '');
document.body.appendChild(textarea);
try {
textarea.select();
textarea.setSelectionRange(0, textarea.value.length);
if (!document.execCommand('copy')) {
throw new Error('copy command failed');
}
} finally {
document.body.removeChild(textarea);
}
}
function copyLink(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text).catch(function () {
fallbackCopy(text);
});
}
try {
fallbackCopy(text);
return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
}
function initShareDropdown() {
var dropdown = document.getElementById('share-dropdown');
var triggers = Array.prototype.slice.call(document.querySelectorAll('#action-share-trigger, .action-share[data-type="share"], .footer-tabbar .action-share')).filter(function (el, idx, arr) { return arr.indexOf(el) === idx; });
var trigger = triggers[0] || null;
if (!trigger || !dropdown || dropdown.dataset.aksrtShareBound === '1') return;
dropdown.dataset.aksrtShareBound = '1';
var qrBox = document.getElementById('share-qrcode-box');
var qrImage = document.getElementById('share-qrcode-image');
function closeDropdown() {
dropdown.setAttribute('aria-hidden', 'true');
triggers.forEach(function (t) { t.setAttribute('aria-expanded', 'false'); });
document.body.classList.remove('aksrt-share-sheet-open');
if (window.AksrtShare && typeof window.AksrtShare.hideQrFallback === 'function') {
window.AksrtShare.hideQrFallback(qrBox);
} else if (qrBox) {
qrBox.classList.remove('is-visible');
qrBox.setAttribute('aria-hidden', 'true');
}
}
function toggleDropdown() {
var isOpen = dropdown.getAttribute('aria-hidden') === 'false';
if (isOpen) {
closeDropdown();
return;
}
dropdown.setAttribute('aria-hidden', 'false');
triggers.forEach(function (t) { t.setAttribute('aria-expanded', 'true'); });
if (window.matchMedia && window.matchMedia('(max-width: 767px)').matches) {
document.body.classList.add('aksrt-share-sheet-open');
}
}
trigger.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
toggleDropdown();
});
triggers.slice(1).forEach(function (extraTrigger) {
extraTrigger.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
// Reuse primary trigger click semantics by synthetic toggle
trigger.click();
});
});
document.addEventListener('click', function (event) {
if (!trigger.contains(event.target) && !dropdown.contains(event.target)) {
closeDropdown();
}
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
closeDropdown();
}
});
dropdown.addEventListener('click', function (event) {
var item = event.target.closest('.share-item');
if (!item) return;
event.preventDefault();
event.stopPropagation();
handleShareAction(item, {
qrBox: qrBox,
qrImage: qrImage,
closeDropdown: closeDropdown,
withinDropdown: true
});
});
}
function handleWeixinShare(node, payload, options) {
options = options || {};
var share = window.AksrtShare;
if (!share || typeof share.openWeixinShare !== 'function') {
notyf('微信分享模块未加载', 'danger');
return Promise.resolve({ mode: 'error' });
}
if (
options.qrBox &&
options.qrBox.classList.contains('is-visible') &&
(!share.isWeixinEnv || !share.isWeixinEnv())
) {
share.hideQrFallback(options.qrBox);
return Promise.resolve({ mode: 'closed' });
}
return share.openWeixinShare({
title: payload.title,
url: payload.url,
desc: payload.desc || payload.text || '',
image: payload.image,
qrBox: options.qrBox || null,
qrImage: options.qrImage || null
}).then(function (result) {
if (result && result.message) {
notyf(result.message, result.mode === 'error' ? 'danger' : (result.mode === 'bridge' ? 'success' : 'info'));
}
return result;
});
}
function handleShareAction(node, options) {
var platform = getPlatform(node);
if (!platform) return Promise.resolve({ mode: 'noop' });
var payload = resolvePayload(node);
if (!payload.url) return Promise.resolve({ mode: 'noop' });
if (platform === 'copy') {
return copyLink(payload.url).then(function () {
notyf('链接已复制', 'success');
if (options && typeof options.closeDropdown === 'function') {
options.closeDropdown();
}
return { mode: 'copy' };
}).catch(function () {
notyf('复制失败,请手动复制链接', 'danger');
return { mode: 'error' };
});
}
if (platform === 'wechat') {
return handleWeixinShare(node, payload, options).then(function (result) {
if (result && result.mode !== 'qr' && result.mode !== 'error' && options && typeof options.closeDropdown === 'function') {
options.closeDropdown();
}
return result;
});
}
if (platform === 'poster') {
if (window.AksrtPosterShare && typeof window.AksrtPosterShare.open === 'function') {
window.AksrtPosterShare.open(payload);
if (options && typeof options.closeDropdown === 'function') options.closeDropdown();
return Promise.resolve({ mode: 'poster' });
}
notyf('海报模块未加载', 'danger');
return Promise.resolve({ mode: 'error' });
}
var shareUrl = buildShareUrl(platform, payload);
if (!shareUrl) {
return Promise.resolve({ mode: 'noop' });
}
openWindow(shareUrl);
if (options && typeof options.closeDropdown === 'function') {
options.closeDropdown();
}
return Promise.resolve({ mode: 'window' });
}
function bindGenericShareButtons() {
document.addEventListener('click', function (event) {
var node = event.target.closest('[data-share-platform]');
if (!node || node.closest('#share-dropdown')) return;
event.preventDefault();
handleShareAction(node, {});
});
document.addEventListener('click', function (event) {
var node = event.target.closest('[data-share-copy]');
if (!node) return;
event.preventDefault();
var url = node.getAttribute('data-share-url') || window.location.href;
copyLink(url).then(function () {
notyf('链接已复制', 'success');
}).catch(function () {
notyf('复制失败,请手动复制链接', 'danger');
});
});
}
function init() {
if (document.documentElement.dataset.aksrtPostShareInitialized === '1') return;
document.documentElement.dataset.aksrtPostShareInitialized = '1';
initShareDropdown();
bindGenericShareButtons();
}
jQuery(document).off('auto_fun.aksrt-post-share').on('auto_fun.aksrt-post-share', function () { init(document); });
})();
+180 -387
View File
@@ -1,406 +1,199 @@
/**
* AKSRT Poster Share — 海报分享
*
* 生成文章分享海报图片,支持下载和分享。
* 使用 Canvas API 绘制,无需外部依赖。
* Reference-theme poster sharing runtime.
*
* @package AKSRT
*/
(function () {
(function (window, document, $) {
'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 || {});
var WIDTH = 768;
var HEIGHT = 1168;
var FONT = '-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",Arial,sans-serif';
this.init();
function notice(message, type) {
if (typeof window.notyf === 'function') window.notyf(message, type || '');
}
PosterShare.prototype.init = function () {
var self = this;
function ajaxUrl() {
return (window.aksrtData && window.aksrtData.ajaxUrl) || (window._win && window._win.ajax_url) || '';
}
document.querySelectorAll(this.options.triggerSelector).forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.preventDefault();
function nonce() {
return (window.aksrtData && window.aksrtData.nonce) || '';
}
// 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)
: {};
function loadImage(sources) {
sources = sources.filter(Boolean);
return new Promise(function (resolve, reject) {
var index = 0;
function next() {
if (index >= sources.length) { reject(new Error('image unavailable')); return; }
var image = new Image();
image.crossOrigin = 'anonymous';
image.onload = function () { resolve(image); };
image.onerror = function () { index += 1; next(); };
image.src = sources[index];
}
next();
});
}
var config = {};
function qrCodeDataUrl(text) {
return new Promise(function (resolve) {
if (!text || typeof window.tbquire !== 'function') { resolve(''); return; }
window.tbquire(['qrcode'], function () {
var host = $('<div>').css({ position: 'fixed', left: '-9999px', top: '-9999px' }).appendTo('body');
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);
host.qrcode({ width: 160, height: 160, correctLevel: 0, text: text, background: '#fff', foreground: '#333' });
var canvas = host.find('canvas').get(0);
resolve(canvas ? canvas.toDataURL('image/png') : '');
} catch (error) {
resolve('');
} finally {
host.remove();
}
});
});
};
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);
function coverRect(boxWidth, boxHeight, sourceWidth, sourceHeight) {
var sx = 0, sy = 0, sw = sourceWidth, sh = sourceHeight;
if (sourceWidth / sourceHeight > boxWidth / boxHeight) {
sw = boxWidth * sourceHeight / boxHeight;
sx = (sourceWidth - sw) / 2;
} else {
sh = boxHeight * sourceWidth / boxWidth;
sy = (sourceHeight - sh) / 2;
}
return { sx: sx, sy: sy, sw: sw, sh: sh };
}
/**
* 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 || ''
}));
};
function containRect(x, y, boxWidth, boxHeight, sourceWidth, sourceHeight) {
var ratio = Math.min(boxWidth / sourceWidth, boxHeight / sourceHeight);
var width = sourceWidth * ratio;
var height = sourceHeight * ratio;
return { x: x + (boxWidth - width) / 2, y: y + (boxHeight - height) / 2, width: width, height: height };
}
window.AksrtPosterShare = PosterShare;
})();
function drawLines(ctx, text, maxWidth, maxRows, x, y, lineHeight) {
var chars = String(text || '').replace(/[\r\n]+/g, ' ').split('');
var rows = [], line = '';
chars.forEach(function (char) {
var next = line + char;
if (line && ctx.measureText(next).width > maxWidth) { rows.push(line); line = char; }
else line = next;
});
if (line) rows.push(line);
if (rows.length > maxRows) {
rows = rows.slice(0, maxRows);
while (rows[maxRows - 1] && ctx.measureText(rows[maxRows - 1] + '...').width > maxWidth) {
rows[maxRows - 1] = rows[maxRows - 1].slice(0, -1);
}
rows[maxRows - 1] += '...';
}
rows.forEach(function (row, index) { ctx.fillText(row, x, y + index * lineHeight); });
}
function renderPoster(data) {
return Promise.all([
loadImage([data.banner, data.banner_default, data.banner_spare]),
data.logo ? loadImage([data.logo]).catch(function () { return null; }) : Promise.resolve(null),
qrCodeDataUrl(data.url).then(function (src) { return src ? loadImage([src]).catch(function () { return null; }) : null; })
]).then(function (assets) {
var banner = assets[0], logo = assets[1], qr = assets[2];
var canvas = document.createElement('canvas');
canvas.width = WIDTH; canvas.height = HEIGHT;
var ctx = canvas.getContext('2d');
var imageHeight = HEIGHT / 2;
ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, WIDTH, HEIGHT);
var crop = coverRect(WIDTH, imageHeight, banner.width, banner.height);
ctx.drawImage(banner, crop.sx, crop.sy, crop.sw, crop.sh, 0, 0, WIDTH, imageHeight);
var now = new Date();
ctx.fillStyle = '#fff'; ctx.font = '35px ' + FONT; ctx.fillText('周' + '日一二三四五六'.charAt(now.getDay()), 30, 70);
ctx.font = '28px ' + FONT; ctx.fillText((now.getMonth() + 1) + ' / ' + now.getDate(), 30, 115);
ctx.strokeStyle = '#00a1ff'; ctx.lineWidth = 7; ctx.lineCap = 'round'; ctx.beginPath();
ctx.moveTo(40, imageHeight + 38); ctx.lineTo(40, imageHeight + 74); ctx.stroke();
ctx.fillStyle = '#424242'; ctx.font = 'bold 35px ' + FONT;
drawLines(ctx, data.title, WIDTH - 120, 2, 58, imageHeight + 68, 52);
ctx.fillStyle = '#fc4e1f'; ctx.font = '24px ' + FONT;
drawLines(ctx, data.tags, WIDTH - 80, 1, 40, imageHeight + 172, 36);
ctx.fillStyle = '#585858'; ctx.font = '25px ' + FONT;
drawLines(ctx, data.content, WIDTH - 80, 4, 40, imageHeight + 232, 39);
ctx.strokeStyle = 'rgba(122,122,122,.2)'; ctx.lineWidth = 2; ctx.strokeRect(30, HEIGHT - 230, WIDTH - 60, 200);
if (logo) {
var logoRect = containRect(80, HEIGHT - 190, 300, 100, logo.width, logo.height);
ctx.drawImage(logo, logoRect.x, logoRect.y, logoRect.width, logoRect.height);
}
if (qr) ctx.drawImage(qr, WIDTH - 200, HEIGHT - 200, 120, 120);
ctx.fillStyle = '#b4b4b4'; ctx.font = '22px ' + FONT; ctx.textAlign = 'center';
ctx.fillText(data.description || '扫一扫 立即查看', WIDTH / 2, HEIGHT - 52);
return canvas.toDataURL('image/png');
});
}
function modalId(postId) { return 'modal_poster_' + postId; }
function createModal(postId) {
var id = modalId(postId);
var html = '<div class="modal fade flex jc" id="' + id + '" tabindex="-1" role="dialog" aria-hidden="true">' +
'<div class="modal-dialog poster-share hover-show" role="document"><div class="modal-content" style="height:220px;overflow:hidden">' +
'<div class="poster-imgbox hover-show"></div><div class="flex jc loading-mask absolute main-bg radius8">' +
'<div class="loading zts fa-2x"></div><div class="muted-2-color box-body">正在生成图片,请稍候...</div></div></div>' +
'<div class="padding-6 abs-center hover-show-con text-center poster-share-actions" style="top:auto">' +
'<a href="#" class="but cir jb-blue poster-download hide"><i class="fa fas fa-download" aria-hidden="true"></i></a>' +
'<button type="button" data-dismiss="modal" class="but cir jb-yellow ml10"><svg aria-hidden="true" class="icon em12"><use xlink:href="#icon-close"></use></svg></button>' +
'</div></div></div>';
$('body').append(html);
return $('#' + id);
}
function showPoster($modal, postId, src) {
var $box = $modal.find('.poster-imgbox').empty().append($('<img>', { src: src, alt: '分享海报', class: 'loaded-img' })).addClass('loaded');
$modal.find('.poster-download').attr({ href: src, download: 'poster_' + postId + '.png' }).removeClass('hide');
$modal.find('.loading-mask').fadeOut(160);
$modal.find('.modal-content').css({ height: $box.outerHeight(), transition: '.2s' });
window.setTimeout(function () { $modal.find('.modal-content').css({ height: '', overflow: '', transition: '' }); }, 220);
}
function requestPoster($trigger, postId, $modal) {
$.ajax({
type: 'POST', url: ajaxUrl(), dataType: 'json',
data: { action: 'aksrt_poster_share_data', id: postId, nonce: nonce() }
}).done(function (response) {
if (!response || !response.success || !response.data) {
notice((response && response.data && response.data.message) || '海报数据加载失败', 'danger');
$modal.modal('hide');
$modal.remove();
return;
}
renderPoster(response.data).then(function (src) { showPoster($modal, postId, src); }).catch(function () {
notice('海报加载失败,请检查图片跨域设置', 'danger');
$modal.modal('hide');
$modal.remove();
});
}).fail(function (xhr) {
var message = xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message;
notice(message || '海报数据加载失败', 'danger');
$modal.modal('hide');
}).always(function () { $trigger.removeAttr('disabled'); });
}
$('body').off('click.aksrtPosterShare', '[poster-share]').on('click.aksrtPosterShare', '[poster-share]', function (event) {
event.preventDefault();
var $trigger = $(this);
var postId = parseInt($trigger.attr('poster-share'), 10) || 0;
if (!postId || $trigger.attr('disabled')) return;
var $modal = $('#' + modalId(postId));
if ($modal.length) { $modal.modal('show'); return; }
$trigger.attr('disabled', 'disabled');
$('.modal').modal('hide');
$modal = createModal(postId);
$modal.modal('show');
requestPoster($trigger, postId, $modal);
});
})(window, document, window.jQuery);
-108
View File
@@ -1,108 +0,0 @@
/**
* AKSRT Share Data Builder
*
* Single source of truth for share payloads. Every share surface
* (dropdown links, copy, WeChat bridge, poster) resolves its data through
* window.AksrtShareData so no script assembles title/url/cover/qrcode on its
* own.
*
* Canonical payload shape:
* { title, url, desc, text, image, qrcode, author, date, siteName }
*
* Data is read from the nearest [data-share-*] context (falling back to
* #share-dropdown), then to OpenGraph/meta tags and document defaults.
*
* @package AKSRT
*/
(function () {
'use strict';
function metaContent(selectors) {
for (var i = 0; i < selectors.length; i += 1) {
var node = document.querySelector(selectors[i]);
var content = node ? node.getAttribute('content') : '';
if (content) return content;
}
return '';
}
function siteName() {
return (
metaContent(['meta[property="og:site_name"]']) ||
(document.querySelector('.site-title, .logo-text, .navbar-logo') || {}).textContent ||
document.title ||
''
).trim();
}
/**
* Find the element that carries the share data attributes for a node.
*/
function context(node) {
var source = node && typeof node.closest === 'function'
? node.closest('[data-share-url], [data-share-title], [data-share-desc], [data-share-image], [data-aksrt-poster]')
: null;
return source || document.getElementById('share-dropdown');
}
/**
* Generate a QR data URL for a link, reusing the WeChat share encoder
* when available. Returns '' if no encoder is loaded.
*/
function qrcode(url) {
if (window.AksrtShare && typeof window.AksrtShare.buildQrDataUrl === 'function' && url) {
return window.AksrtShare.buildQrDataUrl(url, 200) || '';
}
return '';
}
/**
* Resolve a canonical share payload for a triggering node.
*
* @param {Element} node Triggering element (button/link).
* @param {Object} [overrides] Optional explicit overrides.
* @returns {Object}
*/
function resolve(node, overrides) {
var ctx = context(node);
function attr(name) {
return (node && node.getAttribute && node.getAttribute(name)) ||
(ctx && ctx.getAttribute && ctx.getAttribute(name)) || '';
}
var url = attr('data-share-url') || window.location.href;
var payload = {
title: attr('data-share-title') || document.title,
url: url,
desc: attr('data-share-desc') || metaContent(['meta[property="og:description"]', 'meta[name="description"]']),
text: attr('data-share-text') || '',
image: attr('data-share-image') || metaContent(['meta[property="og:image"]']),
qrcode: attr('data-share-qrcode') || '',
author: attr('data-share-author') || '',
date: attr('data-share-date') || '',
siteName: attr('data-share-site') || siteName()
};
if (!payload.qrcode) {
payload.qrcode = qrcode(payload.url);
}
if (overrides && typeof overrides === 'object') {
Object.keys(overrides).forEach(function (key) {
if (overrides[key] !== undefined && overrides[key] !== null && overrides[key] !== '') {
payload[key] = overrides[key];
}
});
}
return payload;
}
window.AksrtShareData = {
resolve: resolve,
qrcode: qrcode,
siteName: siteName
};
})();
-481
View File
@@ -1,481 +0,0 @@
/**
* AKSRT Weixin Share
*
* Bridge-first WeChat share helper with QR fallback.
*
* @package AKSRT
*/
(function () {
'use strict';
/**
* Local QR code data URL generator — no third-party services.
* Uses canvas rendering with a minimal QR matrix encoder.
*/
function buildQRCodeDataUrl(text, size) {
size = size || 160;
try {
var matrix = generateQRMatrix(text || ' ');
if (!matrix) return '';
var canvas = document.createElement('canvas');
var moduleCount = matrix.length;
var scale = Math.floor(size / (moduleCount + 8));
var margin = Math.floor((size - scale * moduleCount) / 2);
canvas.width = size;
canvas.height = size;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = '#000000';
for (var r = 0; r < moduleCount; r++) {
for (var c = 0; c < moduleCount; c++) {
if (matrix[r][c]) {
ctx.fillRect(margin + c * scale, margin + r * scale, scale, scale);
}
}
}
return canvas.toDataURL('image/png');
} catch (e) {
return '';
}
}
/**
* Minimal QR Code matrix generator (Version 1-6, Byte mode, Error Correction L).
* Returns a 2D boolean array or null on failure.
*/
function generateQRMatrix(text) {
var data = encodeUTF8(text);
// Choose version based on data length (EC level L, Byte mode)
var version = 1;
var capacities = [0, 17, 32, 53, 78, 106, 134];
for (var v = 1; v <= 6; v++) {
if (data.length <= capacities[v]) { version = v; break; }
if (v === 6) return null; // data too long for small versions
}
var moduleCount = version * 4 + 17;
var matrix = createMatrix(moduleCount);
var reserved = createMatrix(moduleCount);
// Place finder patterns
placeFinder(matrix, reserved, 0, 0);
placeFinder(matrix, reserved, moduleCount - 7, 0);
placeFinder(matrix, reserved, 0, moduleCount - 7);
// Place alignment patterns (version >= 2)
if (version >= 2) {
var alignPos = getAlignmentPositions(version);
for (var i = 0; i < alignPos.length; i++) {
for (var j = 0; j < alignPos.length; j++) {
if (isInFinder(alignPos[i], alignPos[j], moduleCount)) continue;
placeAlignment(matrix, reserved, alignPos[i], alignPos[j]);
}
}
}
// Place timing patterns
for (var i = 8; i < moduleCount - 8; i++) {
if (!reserved[6][i]) { matrix[6][i] = (i % 2 === 0); reserved[6][i] = true; }
if (!reserved[i][6]) { matrix[i][6] = (i % 2 === 0); reserved[i][6] = true; }
}
// Reserve format info areas
for (var i = 0; i < 8; i++) {
reserved[8][i] = true; reserved[i][8] = true;
reserved[8][moduleCount - 1 - i] = true; reserved[moduleCount - 1 - i][8] = true;
}
reserved[8][8] = true;
if (version >= 2) {
for (var i = moduleCount - 11; i < moduleCount - 8; i++) {
for (var j = 0; j < 6; j++) { reserved[j][i] = true; reserved[i][j] = true; }
}
}
// Dark module
matrix[moduleCount - 8][8] = true;
reserved[moduleCount - 8][8] = true;
// Encode data
var dataBits = encodeData(data, version);
placeData(matrix, reserved, dataBits, moduleCount);
// Apply best mask
var bestMask = 0;
var bestPenalty = Infinity;
for (var m = 0; m < 8; m++) {
var masked = applyMask(matrix, reserved, m, moduleCount);
var penalty = calcPenalty(masked, moduleCount);
if (penalty < bestPenalty) { bestPenalty = penalty; bestMask = m; }
}
matrix = applyMask(matrix, reserved, bestMask, moduleCount);
// Place format info
placeFormatInfo(matrix, bestMask, moduleCount);
return matrix;
}
function createMatrix(n) {
var m = [];
for (var i = 0; i < n; i++) { m[i] = []; for (var j = 0; j < n; j++) m[i][j] = false; }
return m;
}
function encodeUTF8(str) {
var bytes = [];
for (var i = 0; i < str.length; i++) {
var c = str.charCodeAt(i);
if (c < 0x80) bytes.push(c);
else if (c < 0x800) { bytes.push(0xc0 | (c >> 6)); bytes.push(0x80 | (c & 0x3f)); }
else { bytes.push(0xe0 | (c >> 12)); bytes.push(0x80 | ((c >> 6) & 0x3f)); bytes.push(0x80 | (c & 0x3f)); }
}
return bytes;
}
function placeFinder(m, r, row, col) {
for (var dr = -1; dr <= 7; dr++) {
for (var dc = -1; dc <= 7; dc++) {
var rr = row + dr, cc = col + dc;
if (rr < 0 || rr >= m.length || cc < 0 || cc >= m.length) continue;
var val = (dr >= 0 && dr <= 6 && (dc === 0 || dc === 6)) || (dc >= 0 && dc <= 6 && (dr === 0 || dr === 6)) || (dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4);
m[rr][cc] = val;
r[rr][cc] = true;
}
}
}
function placeAlignment(m, r, row, col) {
for (var dr = -2; dr <= 2; dr++) {
for (var dc = -2; dc <= 2; dc++) {
var val = Math.abs(dr) === 2 || Math.abs(dc) === 2 || (dr === 0 && dc === 0);
m[row + dr][col + dc] = val;
r[row + dr][col + dc] = true;
}
}
}
function getAlignmentPositions(v) {
if (v <= 1) return [];
if (v === 2) return [6, 18];
if (v === 3) return [6, 22];
if (v === 4) return [6, 26];
if (v === 5) return [6, 30];
return [6, 34];
}
function isInFinder(row, col, n) {
return (row < 9 && col < 9) || (row < 9 && col >= n - 8) || (row >= n - 8 && col < 9);
}
function encodeData(data, version) {
var bits = [];
// Mode indicator: Byte = 0100
bits.push(0, 1, 0, 0);
// Character count
var ccBits = version <= 9 ? 8 : 16;
for (var i = ccBits - 1; i >= 0; i--) bits.push((data.length >> i) & 1);
// Data
for (var i = 0; i < data.length; i++) {
for (var j = 7; j >= 0; j--) bits.push((data[i] >> j) & 1);
}
// Terminator
var capacities = [0, 152, 272, 440, 640, 864, 1088];
var cap = capacities[version] || 152;
for (var i = 0; i < 4 && bits.length < cap; i++) bits.push(0);
// Pad to byte boundary
while (bits.length % 8 !== 0) bits.push(0);
// Pad bytes
var padBytes = [0xEC, 0x11];
var pi = 0;
while (bits.length < cap) {
var pb = padBytes[pi % 2];
for (var j = 7; j >= 0; j--) bits.push((pb >> j) & 1);
pi++;
}
return bits;
}
function placeData(matrix, reserved, bits, n) {
var bitIndex = 0;
for (var right = n - 1; right >= 1; right -= 2) {
if (right === 6) right = 5;
for (var vert = 0; vert < n; vert++) {
for (var j = 0; j < 2; j++) {
var col = right - j;
var upward = ((right + 1) & 2) === 0;
var row = upward ? n - 1 - vert : vert;
if (!reserved[row][col] && bitIndex < bits.length) {
matrix[row][col] = bits[bitIndex] === 1;
bitIndex++;
}
}
}
}
}
function applyMask(matrix, reserved, maskNum, n) {
var result = createMatrix(n);
for (var r = 0; r < n; r++) {
for (var c = 0; c < n; c++) {
result[r][c] = matrix[r][c];
if (!reserved[r][c]) {
var invert = false;
switch (maskNum) {
case 0: invert = (r + c) % 2 === 0; break;
case 1: invert = r % 2 === 0; break;
case 2: invert = c % 3 === 0; break;
case 3: invert = (r + c) % 3 === 0; break;
case 4: invert = (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0; break;
case 5: invert = (r * c) % 2 + (r * c) % 3 === 0; break;
case 6: invert = ((r * c) % 2 + (r * c) % 3) % 2 === 0; break;
case 7: invert = ((r + c) % 2 + (r * c) % 3) % 2 === 0; break;
}
if (invert) result[r][c] = !result[r][c];
}
}
}
return result;
}
function calcPenalty(matrix, n) {
var penalty = 0;
// Rule 1: consecutive same-color modules in row/column
for (var r = 0; r < n; r++) {
var run = 1;
for (var c = 1; c < n; c++) {
if (matrix[r][c] === matrix[r][c - 1]) { run++; } else { run = 1; }
if (run === 5) penalty += 3;
else if (run > 5) penalty += 1;
}
}
for (var c = 0; c < n; c++) {
var run = 1;
for (var r = 1; r < n; r++) {
if (matrix[r][c] === matrix[r - 1][c]) { run++; } else { run = 1; }
if (run === 5) penalty += 3;
else if (run > 5) penalty += 1;
}
}
return penalty;
}
function placeFormatInfo(matrix, maskNum, n) {
var formatInfo = getFormatInfo(maskNum);
// Place around top-left finder
var positions = [
[8, 0], [8, 1], [8, 2], [8, 3], [8, 4], [8, 5], [8, 7], [8, 8],
[7, 8], [5, 8], [4, 8], [3, 8], [2, 8], [1, 8], [0, 8]
];
for (var i = 0; i < 15; i++) {
var r = positions[i][0], c = positions[i][1];
matrix[r][c] = formatInfo[i] === 1;
}
// Place around bottom-left and top-right finders
var positions2 = [
[n - 1, 8], [n - 2, 8], [n - 3, 8], [n - 4, 8], [n - 5, 8], [n - 6, 8], [n - 7, 8],
[8, n - 8], [8, n - 7], [8, n - 6], [8, n - 5], [8, n - 4], [8, n - 3], [8, n - 2], [8, n - 1]
];
for (var i = 0; i < 15; i++) {
var r = positions2[i][0], c = positions2[i][1];
matrix[r][c] = formatInfo[i] === 1;
}
}
function getFormatInfo(maskNum) {
// EC level L = 01, mask number = maskNum
var data = (0x01 << 3) | maskNum; // 5 bits
var bits = data << 10;
// BCH(15,5) error correction
var gen = 0x537; // generator polynomial
for (var i = 14; i >= 10; i--) {
if (bits & (1 << i)) bits ^= gen << (i - 10);
}
var formatBits = (data << 10) | bits;
// XOR with mask pattern
formatBits ^= 0x5412;
var result = [];
for (var i = 14; i >= 0; i--) result.push((formatBits >> i) & 1);
return result;
}
function isWeixinEnv() {
return /micromessenger/i.test(window.navigator.userAgent || '') || !!window.WeixinJSBridge;
}
function normalizePayload(payload) {
payload = payload || {};
return {
title: payload.title || document.title,
url: payload.url || window.location.href,
desc: payload.desc || '',
image: payload.image || ''
};
}
function hideQrFallback(qrBox) {
if (!qrBox) return;
qrBox.classList.remove('is-visible');
qrBox.setAttribute('aria-hidden', 'true');
}
function showQrFallback(payload) {
var qrBox = payload.qrBox;
var qrImage = payload.qrImage;
if (!qrBox || !qrImage || !payload.url) {
return Promise.resolve({
mode: 'error',
message: '当前页面缺少微信二维码容器'
});
}
if (qrImage.dataset.qrValue !== payload.url || !qrImage.getAttribute('src')) {
qrImage.dataset.qrValue = payload.url;
var dataUrl = buildQRCodeDataUrl(payload.url, 160);
if (dataUrl) {
qrImage.src = dataUrl;
} else {
qrImage.alt = '二维码生成失败,请直接复制链接分享';
}
}
qrBox.classList.add('is-visible');
qrBox.setAttribute('aria-hidden', 'false');
return Promise.resolve({
mode: 'qr',
message: '请使用微信扫一扫分享'
});
}
function bridgeWithWxSdk(payload) {
return new Promise(function (resolve, reject) {
if (!window.wx || typeof window.wx.ready !== 'function') {
reject(new Error('wx sdk unavailable'));
return;
}
var done = false;
var timer = window.setTimeout(function () {
if (done) return;
done = true;
reject(new Error('微信分享桥接未就绪'));
}, 1200);
function finishSuccess() {
if (done) return;
done = true;
window.clearTimeout(timer);
resolve({
mode: 'bridge',
message: '已同步微信分享信息,请使用右上角菜单继续分享'
});
}
function finishError(error) {
if (done) return;
done = true;
window.clearTimeout(timer);
reject(error instanceof Error ? error : new Error('微信分享桥接失败'));
}
try {
if (typeof window.wx.error === 'function') {
window.wx.error(function (error) {
finishError(new Error((error && error.errMsg) || '微信签名校验失败'));
});
}
window.wx.ready(function () {
try {
if (typeof window.wx.updateAppMessageShareData === 'function') {
window.wx.updateAppMessageShareData({
title: payload.title,
desc: payload.desc,
link: payload.url,
imgUrl: payload.image
});
}
if (typeof window.wx.updateTimelineShareData === 'function') {
window.wx.updateTimelineShareData({
title: payload.title,
link: payload.url,
imgUrl: payload.image
});
}
finishSuccess();
} catch (error) {
finishError(error);
}
});
} catch (error) {
finishError(error);
}
});
}
function bridgeWithWeixinJsBridge(payload) {
return new Promise(function (resolve, reject) {
var bridge = window.WeixinJSBridge;
if (!bridge || typeof bridge.invoke !== 'function') {
reject(new Error('WeixinJSBridge unavailable'));
return;
}
try {
bridge.invoke('sendAppMessage', {
title: payload.title,
desc: payload.desc,
link: payload.url,
img_url: payload.image
}, function (response) {
var errMsg = response && response.err_msg ? response.err_msg : '';
if (!errMsg || /:ok$/i.test(errMsg)) {
resolve({
mode: 'bridge',
message: '已调起微信分享面板'
});
return;
}
reject(new Error(errMsg));
});
} catch (error) {
reject(error);
}
});
}
function openWeixinShare(payload) {
var data = normalizePayload(payload);
data.qrBox = payload && payload.qrBox ? payload.qrBox : null;
data.qrImage = payload && payload.qrImage ? payload.qrImage : null;
if (!isWeixinEnv()) {
return showQrFallback(data);
}
return bridgeWithWxSdk(data)
.catch(function () {
return bridgeWithWeixinJsBridge(data);
})
.catch(function (error) {
return showQrFallback(data).then(function (result) {
result.message = (error && error.message ? error.message + ',' : '') + '已切换为二维码分享';
return result;
});
});
}
window.AksrtShare = Object.assign({}, window.AksrtShare || {}, {
isWeixinEnv: isWeixinEnv,
hideQrFallback: hideQrFallback,
showQrFallback: showQrFallback,
openWeixinShare: openWeixinShare,
// Exposed so the unified share-data-builder can render poster QR codes
// without a second encoder.
buildQrDataUrl: buildQRCodeDataUrl
});
})();