diff --git a/assets/admin/src/schema/sections/payment/affiliate.js b/assets/admin/src/schema/sections/payment/affiliate.js index c1526210..b6592043 100644 --- a/assets/admin/src/schema/sections/payment/affiliate.js +++ b/assets/admin/src/schema/sections/payment/affiliate.js @@ -35,6 +35,8 @@ export const affiliateSchema = { pay_rebate_poster_img: '', pay_rebate_poster_title: '发现一个精品网站,快来看一看', pay_rebate_poster_desc: '网站有超多精品资源和优惠福利,扫码进入网站还可享受诸多优惠哦', + pay_rebate_poster_banner: '', + pay_rebate_poster_desc_bottom: '', pay_rebate_text_discount: '推荐购买 下单再减%discount%', }, groups: [ @@ -173,6 +175,8 @@ export const affiliateSchema = { { key: 'pay_rebate_poster_img', label: '推广海报顶部图片', type: 'media', default: '', dependsOn: { key: 'pay_rebate_poster_s', value: true } }, { key: 'pay_rebate_poster_title', label: '推广海报标题(20-30个字)', type: 'input', default: '发现一个精品网站,快来看一看', dependsOn: { key: 'pay_rebate_poster_s', value: true } }, { key: 'pay_rebate_poster_desc', label: '推广海报简介(40-60个字)', type: 'input', default: '网站有超多精品资源和优惠福利,扫码进入网站还可享受诸多优惠哦', dependsOn: { key: 'pay_rebate_poster_s', value: true } }, + { key: 'pay_rebate_poster_banner', label: '海报顶部备用图片', type: 'media', default: '', dependsOn: { key: 'pay_rebate_poster_s', value: true }, help: '顶部主图加载失败时依次回退到这里,再回退到主题内置图,避免海报顶部出现空白。' }, + { key: 'pay_rebate_poster_desc_bottom', label: '海报底部文案', type: 'input', default: '扫描二维码阅读全文', dependsOn: { key: 'pay_rebate_poster_s', value: true }, help: '画在二维码下方居中位置,留空则使用默认文案。' }, ], }, { diff --git a/assets/js/credits.js b/assets/js/credits.js index 4934dea8..38d882dc 100644 --- a/assets/js/credits.js +++ b/assets/js/credits.js @@ -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 开通 ===== diff --git a/src/Admin/Schema/SchemaPayment.php b/src/Admin/Schema/SchemaPayment.php index 58e60a14..b2ccc977 100644 --- a/src/Admin/Schema/SchemaPayment.php +++ b/src/Admin/Schema/SchemaPayment.php @@ -421,6 +421,8 @@ class SchemaPayment { ['key' => 'pay_rebate_poster_img', 'label' => '推广海报顶部图片', 'type' => 'media', 'default' => '', 'dependsOn' => ['key' => 'pay_rebate_poster_s', 'value' => true]], ['key' => 'pay_rebate_poster_title', 'label' => '推广海报标题(20-30个字)', 'type' => 'input', 'default' => '发现一个精品网站,快来看一看', 'dependsOn' => ['key' => 'pay_rebate_poster_s', 'value' => true]], ['key' => 'pay_rebate_poster_desc', 'label' => '推广海报简介(40-60个字)', 'type' => 'input', 'default' => '网站有超多精品资源和优惠福利,扫码进入网站还可享受诸多优惠哦', 'dependsOn' => ['key' => 'pay_rebate_poster_s', 'value' => true]], + ['key' => 'pay_rebate_poster_banner', 'label' => '海报顶部备用图片', 'type' => 'media', 'default' => '', 'dependsOn' => ['key' => 'pay_rebate_poster_s', 'value' => true], 'help' => '顶部主图加载失败时依次回退到这里,再回退到主题内置图,避免海报顶部出现空白。'], + ['key' => 'pay_rebate_poster_desc_bottom', 'label' => '海报底部文案', 'type' => 'input', 'default' => '扫描二维码阅读全文', 'dependsOn' => ['key' => 'pay_rebate_poster_s', 'value' => true], 'help' => '画在二维码下方居中位置,留空则使用默认文案。'], ]], ['key' => 'discount', 'title' => '推广让利标签', 'fields' => [ ['key' => 'discount_notice', 'label' => '', 'type' => 'content', 'ui_only' => true, 'content' => '
推广让利标签配置详解:
%discount%、推荐人姓名 %referrer_name%%referrer_name%推荐购买 下单再减%discount%元