feat(share): align social sharing runtime
This commit is contained in:
1 parent
26ac8167f3
commit
cf6c11852e
33 files changed
+802
-1919
No files matched your search
@@ -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
@@ -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);
|
||||
@@ -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
|
||||
};
|
||||
})();
|
||||
@@ -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
|
||||
});
|
||||
})();
|
||||
Reference in new issue
Block a user