/** * 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 }); })();