Files
wordpress-theme-aksrt/assets/js/credits-affiliate.js
T
kate522andClaude Opus 5 427968a721 chore(branding): remove leftover upstream theme identifiers
The theme was renamed Zibll/子比 → AKSRT → Jio, but upstream branding
survived the sweeps in CSS classes, the SVG sprite prefix, a persisted
widget field ID, JS events, admin Vue class names, test infrastructure
and docblocks.

Renames:
- zib-widget / zib-widget-wrap / zib-slider → jio-widget-box /
  jio-widget-box-wrap / jio-slider-box (jio-widget is taken by the modern
  widget shell, jio-box-* by the card primitives)
- zib-partners-scroll + zps-* → jio-partners-scroll + jps-*
- zib-combo-info-card + zci-* → jio-combo-info-card + jci-*
- zibsvg- → jiosvg-, including the exported picker helpers and the
  fa-zibsvg- family-prefix guard
- zibpay_type → pay_type; the dual-read fallback in PostQuery collapses
  to a single key
- [zibajax="submit"] dropped (no PHP emitter); zib_ajax.success →
  jio_ajax.success
- aks-* admin Vue classes → jio-*
- zib_palette() → palette()

User-facing vendor traces are gone: the floating "more" button default no
longer ships a zibll.com link or 子比主题 marketing copy, the notice
widget ships no preset promo entries, and the four 官网教程 links are
removed from the post-list admin schema.

Test infrastructure renamed to neutral names (docker-compose.reference.yml,
Caddyfile, article-baseline.json, modern-widgets-contract.test.cjs) with
their DB/volume/env/slug identifiers following.

Kept intentionally, documented in tests/fixtures/README.md: the golden
fixture reference fingerprint (a SHA-256 record of an external directory,
read by validate-widget-golden / generate-widget-golden-fixture /
audit-widget-system), the doesNotMatch guards that prove the old tokens are
gone, and the reference theme's own meta key in the visual fixtures.

No backward-compat or data migration: saved widget configs holding old
icon keys or the old field ID silently lose them, per the project's
rewrite-over-compatibility rule.

Verified: 132/132 node contract tests, php tests/widget-system/run.php
(146 assertions), audit-admin-contracts, audit-psr4-paths and
validate-widget-golden all green; tracked bundles regenerated and in sync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-02 00:06:54 +08:00

343 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Jio affiliate revenue center + referral poster (split from credits.js).
* Exposes window.JioAffiliateRevenueInit for the user-center router.
*
* @package Jio
*/
(function () {
"use strict";
function api(path, options) {
return window.JioRequest.request(path, options || {}).then(function (response) {
return { ok: response.ok, status: response.status, json: response.json || {}, data: response.data };
}).catch(function (error) {
var response = error && error.response ? error.response : {};
return { ok: false, status: (error && error.status) || response.status || 0, json: (error && error.json) || response.json || {} };
});
}
// ===== Forward-only affiliate revenue center =====
/* 推广链接由 PHP 直出,首屏即可见、可复制;这里只补异步才拿得到的
* 海报配置与佣金比例,取失败也不影响复制。 */
function initAffiliateRevenue() {
var root = document.querySelector('[data-jio-revenue-page]');
if (!root || root.dataset.affiliateBound) return;
root.dataset.affiliateBound = '1';
var input = root.querySelector('[data-affiliate-link]');
var copy = root.querySelector('[data-affiliate-copy]');
var url = input ? input.value : '';
if (copy) copy.disabled = !url;
api('/revenue/me').then(function (r) {
if (!r.ok) return;
var j = r.json || {};
var ratio = root.querySelector('[data-affiliate-ratio]'); if (ratio) ratio.textContent = String(j.current_ratio || 0) + '%';
initAffiliatePoster(root, url || j.url || '', j.poster || {}, j.site_name || document.title, j.referrer_name || '');
}).catch(function () { /* 海报与比例为增强项,静默失败即可。 */ });
if (copy) copy.addEventListener('click', function () {
if (!input || !input.value) return;
var done = function () { notyf('推广链接已复制', 'success', 3, 'jio-affiliate-copy-toast'); };
var fail = function () { input.focus(); input.select(); notyf('复制失败,请手动复制', 'danger', 3, 'jio-affiliate-copy-toast'); };
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(input.value).then(done).catch(fail);
else { input.focus(); input.select(); try { document.execCommand('copy'); done(); } catch (e) { fail(); } }
});
}
// ===== Referral poster =====
/* 布局 1:1 复刻参考主题 poster-share.js 的 rebate 分支(非 product):
* 768×1168,顶部主图占上半屏、左上角日期、标题竖线 + 标题、红色推荐人标签层、
* 简介,底部带边框盒内左侧 contain 居中 logo 图 + 右下二维码,最底部居中文案。
* 顶部主图按参考主题的三级回退加载(banner → banner_default → banner_spare)。 */
function initAffiliatePoster(root, url, poster, siteName, referrerName) {
var button = root.querySelector('[data-affiliate-poster]');
var modal = document.querySelector('[data-jio-affiliate-poster-modal]');
var stage = modal ? modal.querySelector('[data-affiliate-poster-stage]') : null;
var loading = modal ? modal.querySelector('[data-affiliate-poster-loading]') : null;
var content = modal ? modal.querySelector('.modal-content') : null;
var download = modal ? modal.querySelector('[data-affiliate-poster-download]') : null;
if (!button || !modal) return;
if (!url || !poster || poster.enabled !== true) return;
button.hidden = false;
var backdrop = null;
var canvas = null;
function openPosterModal() {
if (modal.classList.contains('in')) return;
modal.style.display = 'flex';
void modal.offsetWidth; // 强制 reflow,让 .fade 过渡从 scale(0.9) 起手
modal.classList.add('in');
modal.setAttribute('aria-hidden', 'false');
if (!backdrop) {
backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop fade in';
backdrop.setAttribute('data-affiliate-poster-backdrop', '1');
document.body.appendChild(backdrop);
}
document.body.classList.add('modal-open');
}
function closePosterModal() {
if (!modal.classList.contains('in')) return;
modal.classList.remove('in');
modal.setAttribute('aria-hidden', 'true');
document.body.classList.remove('modal-open');
if (backdrop && backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
backdrop = null;
// 等过渡结束再撤 display,复刻 Bootstrap 的 fade-out 收尾
window.setTimeout(function () {
if (!modal.classList.contains('in')) modal.style.display = '';
}, 300);
}
modal.addEventListener('click', function (event) {
// 点遮罩层(.modal 自身)或关闭按钮(data-dismiss="modal")
if (event.target === modal || (event.target.closest && event.target.closest('[data-dismiss="modal"]'))) {
closePosterModal();
}
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && modal.classList.contains('in')) closePosterModal();
});
if (download) {
download.addEventListener('click', function () {
if (!canvas) return;
try {
var link = document.createElement('a');
link.download = 'referral-poster.png';
link.href = canvas.toDataURL('image/png');
document.body.appendChild(link);
link.click();
link.remove();
} catch (err) {
/* Canvas is same-origin generated; download can only fail if the browser blocks it. */
}
});
}
button.addEventListener('click', function () {
if (typeof window.qrcode !== 'function') {
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '二维码组件未加载。' + '</p>'; }
openPosterModal();
return;
}
if (stage) { stage.innerHTML = ''; stage.classList.remove('loaded'); }
if (loading) loading.style.display = '';
if (download) download.classList.add('hide');
openPosterModal();
button.disabled = true;
buildPosterCanvas(url, poster, siteName, referrerName).then(function (built) {
canvas = built;
if (!stage) return;
// 参考主题:img.loaded-img 直接进 .poster-imgbox,遮罩淡出,
// 内容块高度从 220px 过渡到海报实际高度后清掉内联样式。
var img = document.createElement('img');
img.src = canvas.toDataURL('image/png');
img.alt = '推广海报';
img.className = 'loaded-img';
// 参考主题按 0.7 比例预设尺寸;.poster-imgbox > img 的 max-width:360px
// 会在窄屏把它压回弹窗宽度,height:100% 让 offsetHeight 反映真实渲染高度。
img.width = 768 * 0.7;
img.height = 1168 * 0.7;
stage.appendChild(img);
stage.classList.add('loaded');
if (download) {
download.href = img.src;
download.classList.remove('hide');
}
if (loading) loading.style.display = 'none';
if (content) {
content.style.height = stage.offsetHeight + 'px';
content.style.transition = '.2s';
window.setTimeout(function () {
content.style.height = '';
content.style.overflow = '';
content.style.transition = '';
}, 200);
}
}).catch(function () {
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '海报生成失败,请重试。' + '</p>'; }
}).finally(function () {
button.disabled = false;
});
});
}
// 三级回退加载图片:任一源成功即返回,全部失败才 reject(参考主题 loadCount 0→1→2)。
function loadPosterImage(sources) {
var list = (sources || []).filter(function (src) { return !!src; });
return new Promise(function (resolve, reject) {
var index = 0;
function next() {
if (index >= list.length) { reject(new Error('poster image unavailable')); return; }
var image = new Image();
image.crossOrigin = 'anonymous';
image.onload = function () { resolve(image); };
image.onerror = function () { index += 1; next(); };
image.src = list[index];
}
next();
});
}
// contain 裁切:参考主题 containImg(),等比缩放塞进盒子并居中,不裁切。
function containRect(sx, sy, boxW, boxH, srcW, srcH) {
var dx = sx, dy = sy, dWidth = boxW, dHeight = boxH;
if (srcW > srcH || (srcW === srcH && boxW < boxH)) {
dHeight = (srcH * dWidth) / srcW;
dy = sy + (boxH - dHeight) / 2;
} else if (srcW < srcH || (srcW === srcH && boxW > boxH)) {
dWidth = (srcW * dHeight) / srcH;
dx = sx + (boxW - dWidth) / 2;
}
return { x: dx, y: dy, width: dWidth, height: dHeight };
}
// 参考主题 drawTextOverflow():按 maxWidth-120 换行、x 偏移 38,最多 maxRow 行,溢出补 ...。
// 返回实际测量宽度,供价格/标签层做横向排版。
function drawTextOverflow(ctx, text, maxWidth, maxRow, font, color, lineHeight, x, y) {
var rows = [];
var temp = '';
var limit = maxWidth - 120;
x = x + 38;
text = String(text || '').replace(/[\r\n]/g, '');
ctx.fillStyle = color;
ctx.font = font;
var chars = text.split('');
var measured = 0;
if (ctx.measureText(text).width <= limit) {
rows.push(text);
} else {
for (var i = 0; i < chars.length; i++) {
if (rows.length === maxRow && i < chars.length - 1) {
rows[rows.length - 1] += '...';
break;
}
if (ctx.measureText(temp).width < limit) {
temp += chars[i];
if (i === chars.length - 1) rows.push(temp);
} else {
i--; // 参考主题:回退一格防止字符丢失
rows.push(temp);
temp = '';
}
}
}
for (var r = 0; r < rows.length; r++) {
measured = Math.max(measured, ctx.measureText(rows[r]).width);
ctx.fillText(rows[r], x, y + r * lineHeight, limit);
}
return { width: measured, height: rows.length * lineHeight };
}
// 参考主题 buildPosterCanvas rebate 分支:768×1168,顶部主图占上半屏,左上角日期,
// 标题竖蓝线 + 标题,推荐人标签(红),简介,底部盒(logo + 二维码 + 底部文案)。
function buildPosterCanvas(url, poster, siteName, referrerName) {
var width = 768, height = 1168;
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
var imgHeight = height / 2; // rebate 分支:顶部主图占上半屏
// 站点名走「周」前缀。
var weekLabel = '周' + '日一二三四五六'.charAt(new Date().getDay());
// 推荐人标签:参考主题 tags = `推荐人: {昵称}`,缺昵称时整层不画。
var tags = referrerName ? '推荐人: ' + String(referrerName) : '';
// 底部文案:优先推广海报配置,回退到分享设置 share_desc,再回退默认文案。
var bottom = String(poster.bottom || '扫描二维码阅读全文');
var title = String(poster.title || siteName || '');
var description = String(poster.description || '');
return Promise.all([
loadPosterImage([poster.image, poster.banner_default, poster.banner_spare]),
poster.logo ? loadPosterImage([poster.logo]).catch(function () { return null; }) : Promise.resolve(null)
]).then(function (assets) {
var banner = assets[0], logo = assets[1];
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
// 顶部主图(cover 裁切)。
var rect = coverRect(width, imgHeight, banner.width, banner.height);
ctx.drawImage(banner, rect.sx, rect.sy, rect.sWidth, rect.sHeight, 0, 0, width, imgHeight);
// 左上角日期(白字,覆盖在主图上)。
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.fillStyle = 'rgba(255,255,255,1)';
ctx.font = '35px Arial';
ctx.fillText(weekLabel, 30, 37);
ctx.font = '28px Arial';
ctx.fillText((new Date().getMonth() + 1) + ' / ' + new Date().getDate(), 30, 87);
// 标题竖蓝线。
ctx.strokeStyle = '#00a1ff';
ctx.lineWidth = 7;
ctx.beginPath();
ctx.lineCap = 'round';
ctx.moveTo(40, imgHeight + 38);
ctx.lineTo(40, imgHeight + 74);
ctx.stroke();
// 标题层(粗体)。参考 rebate 分支把 $title 子画布置于 (20, img_height+40),
// 子画布内左对齐 x=40、文字基线偏移 2,故绝对坐标 (40, img_height+42);
// drawTextOverflow 内部会 +38,所以这里传 2。
drawTextOverflow(ctx, title, width, 2, 'bold 35px Arial', '#424242', 52, 2, imgHeight + 42);
// 推荐人标签层(红,最多 1 行):$tags 置于 (0, img_height+160)。
if (tags) {
drawTextOverflow(ctx, tags, width, 1, '24px Arial', '#fc4e1f', 36, 2, imgHeight + 162);
}
// 简介层(灰):$content 置于 (0, img_height+220)。
if (description) {
drawTextOverflow(ctx, description, width, 4, '25px Arial', 'rgba(88,88,88,1)', 39, 2, imgHeight + 222);
}
// 底部带边框盒。
ctx.strokeStyle = 'rgba(122,122,122,0.2)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.rect(30, height - 230, width - 60, 200);
ctx.stroke();
// logo 图:contain 居中,缺图时整块不画(参考主题 logo.naturalWidth 守卫)。
if (logo && logo.width && logo.height) {
var logoRect = containRect(80, height - 190, 300, 100, logo.width, logo.height);
ctx.drawImage(logo, logoRect.x, logoRect.y, logoRect.width, logoRect.height);
}
// 二维码(右下角)。
var qr = window.qrcode(0, 'M');
qr.addData(url);
qr.make();
var count = qr.getModuleCount();
var qrBox = 120;
var cell = qrBox / count;
var qrX = width - 200, qrY = height - 200;
ctx.fillStyle = '#0f172a';
for (var r = 0; r < count; r++) {
for (var c = 0; c < count; c++) {
if (qr.isDark(r, c)) {
ctx.fillRect(qrX + Math.floor(c * cell), qrY + Math.floor(r * cell), Math.ceil(cell), Math.ceil(cell));
}
}
}
// 底部文案(居中,盒下方)。
ctx.textAlign = 'center';
ctx.fillStyle = '#b4b4b4';
ctx.font = '22px Arial';
ctx.fillText(String(bottom).slice(0, 46), width / 2, height - 58);
ctx.textAlign = 'left';
return canvas;
});
}
// cover 裁切:把源图按短边铺满目标框并居中裁切。
function coverRect(boxW, boxH, srcW, srcH) {
var sx = 0, sy = 0, sWidth = srcW, sHeight = srcH;
if (srcW / srcH > boxW / boxH) {
sWidth = (boxW * srcH) / boxH;
sx = (srcW - sWidth) / 2;
} else {
sHeight = (boxH * srcW) / boxW;
sy = (srcH - sHeight) / 2;
}
return { sx: sx, sy: sy, sWidth: sWidth, sHeight: sHeight };
}
window.JioAffiliateRevenueInit = initAffiliateRevenue;
jQuery(document).off("auto_fun.jio-credits-affiliate").on("auto_fun.jio-credits-affiliate", initAffiliateRevenue);
})();