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