fix(revenue): replicate the reference rebate poster canvas layers

The poster modal shell already matched the reference theme, but the canvas
drawing diverged from zibll's poster-share.js rebate branch in four places:

- No 推荐人 tags layer. The reference paints a red #fc4e1f 24px line at
  img_height+160; AKSRT dropped the layer entirely, so the poster lost the
  referrer attribution that identifies whose link it is.
- The logo slot painted text. The reference contains the real logo image
  into (80, height-190, 300, 100); AKSRT drew the site name as a 30px string
  plus a hardcoded 长按识别二维码访问 caption in that slot.
- Bottom copy was hardcoded. The reference reads share_desc; now sourced
  from the new pay_rebate_poster_desc_bottom field with share_desc fallback.
- No banner fallback. The reference walks banner → banner_default →
  banner_spare; a single broken URL left the top half of the poster black.

Also aligns the week label to the reference's 周 prefix (was 星期日) and ports
drawTextOverflow for wrapping/truncation, which retires the local wrapText.

Adds pay_rebate_poster_banner and pay_rebate_poster_desc_bottom to both the
Vue and PHP schemas; rest_me now returns logo, compatible and referrer_name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-10-21 21:07:48 +08:00
1 parent ef15416ef7
commit 81b60eb427
7 files changed
+212 -95

No files matched your search

+124 -86
View File
@@ -139,7 +139,7 @@
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);
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;
@@ -151,10 +151,11 @@
}
// ===== Referral poster =====
// 弹窗形态 1:1 复刻参考主题 poster-share.js:Bootstrap 语义(.in 显隐 +
// .modal-backdrop + body.modal-open + ESC),与下方充值弹窗同构,不复依赖
// jQuery/Bootstrap 运行时。壳上带 .flex.jc,故打开态用 flex 而非 block。
function initAffiliatePoster(root, url, poster, siteName) {
/* 布局 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-aksrt-affiliate-poster-modal]');
var stage = modal ? modal.querySelector('[data-affiliate-poster-stage]') : null;
@@ -233,7 +234,7 @@
if (download) download.classList.add('hide');
openPosterModal();
button.disabled = true;
buildPosterCanvas(url, poster, siteName).then(function (built) {
buildPosterCanvas(url, poster, siteName, referrerName).then(function (built) {
canvas = built;
if (!stage) return;
// 参考主题:img.loaded-img 直接进 .poster-imgbox,遮罩淡出,
@@ -270,89 +271,141 @@
});
}
// 参考主题 poster-share.js 布局 1:1:768×1168,顶部主图占上半屏,
// 左上角日期,标题竖蓝线 + 标题,正文,底部带边框盒(logo/站名 + 二维码 + 链接)。
function buildPosterCanvas(url, poster, siteName) {
// 三级回退加载图片:任一源成功即返回,全部失败才 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');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
var loadImage = null;
if (poster.image) {
loadImage = new Promise(function (resolve) {
var image = new Image();
image.crossOrigin = 'anonymous';
image.onload = function () { resolve(image); };
image.onerror = function () { resolve(null); };
image.src = poster.image;
});
} else {
loadImage = Promise.resolve(null);
}
return loadImage.then(function (image) {
var imgHeight = height / 2; // 参考主题 rebate(非 product):顶部主图占上半屏。
// 顶部主图(cover 裁切)或纯色占位。
if (image && image.width && image.height) {
var rect = coverRect(width, imgHeight, image.width, image.height);
ctx.drawImage(image, rect.sx, rect.sy, rect.sWidth, rect.sHeight, 0, 0, width, imgHeight);
} else {
ctx.fillStyle = '#0f172a';
ctx.fillRect(0, 0, width, imgHeight);
}
var imgHeight = height / 2; // rebate 分支:顶部主图占上半屏
// 站点名走「周」前缀,与参考主题 zib__('week_prefix') 一致。
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);
// 左上角日期(白字,覆盖在主图上)。
var now = new Date();
var week = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.fillStyle = 'rgba(255,255,255,0.95)';
ctx.font = 'bold 35px Arial, sans-serif';
ctx.fillText(week[now.getDay()], 40, 35);
ctx.font = '28px Arial, sans-serif';
ctx.fillText((now.getMonth() + 1) + ' / ' + now.getDate(), 40, 88);
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.lineCap = 'round';
ctx.beginPath();
ctx.lineCap = 'round';
ctx.moveTo(40, imgHeight + 38);
ctx.lineTo(40, imgHeight + 74);
ctx.stroke();
// 标题(粗体,最多 2 行)。
ctx.fillStyle = '#424242';
ctx.font = 'bold 35px Arial, sans-serif';
var titleLines = wrapText(ctx, String(poster.title || siteName || ''), width - 100).slice(0, 2);
var cursorY = imgHeight + 40;
for (var t = 0; t < titleLines.length; t++) {
ctx.fillText(titleLines[t], 60, cursorY);
cursorY += 52;
// 标题层(粗体)。参考 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);
}
// 正文(简介,最多 3 行)。
var description = String(poster.description || '');
// 简介层(灰):$content 置于 (0, img_height+220)。
if (description) {
ctx.fillStyle = '#585858';
ctx.font = '25px Arial, sans-serif';
var lines = wrapText(ctx, description, width - 80).slice(0, 3);
var contentY = imgHeight + 160;
for (var i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], 40, contentY);
contentY += 38;
}
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.strokeRect(30, height - 230, width - 60, 200);
// 站点名(logo 位,左侧)。
ctx.fillStyle = '#424242';
ctx.font = 'bold 30px Arial, sans-serif';
ctx.fillText(String(siteName || ''), 80, height - 175);
ctx.fillStyle = '#94a3b8';
ctx.font = '20px Arial, sans-serif';
ctx.fillText('长按识别二维码访问', 80, height - 130);
// 二维码(右下)。
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();
@@ -368,11 +421,11 @@
}
}
}
// 底部链接(居中)。
// 底部文案(居中,盒下方)。
ctx.textAlign = 'center';
ctx.fillStyle = '#b4b4b4';
ctx.font = '22px Arial, sans-serif';
ctx.fillText(String(url).slice(0, 46), width / 2, height - 46);
ctx.font = '22px Arial';
ctx.fillText(String(bottom).slice(0, 46), width / 2, height - 58);
ctx.textAlign = 'left';
return canvas;
});
@@ -391,21 +444,6 @@
return { sx: sx, sy: sy, sWidth: sWidth, sHeight: sHeight };
}
function wrapText(ctx, text, maxWidth) {
var lines = [];
var current = '';
for (var i = 0; i < text.length; i++) {
if (ctx.measureText(current + text[i]).width > maxWidth && current) {
lines.push(current);
current = text[i];
} else {
current += text[i];
}
}
if (current) lines.push(current);
return lines;
}
window.AKSRTAffiliateRevenueInit = initAffiliateRevenue;
// ===== VIP 开通 =====