482 lines
15 KiB
JavaScript
482 lines
15 KiB
JavaScript
/**
|
||
* 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
|
||
});
|
||
})();
|