| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport"
- content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
- <title></title>
- <script src="/js/url-cache.js"></script>
- <script src="/token.js"></script>
- <link rel="stylesheet" href="/css/business-theme.css">
- <link rel="stylesheet" href="/css/invite-activity.css">
- <link rel="preload" as="image" href="/images/group-470833-bg.png">
- <!-- 关键图片预加载占位,后续由业务方补充 -->
- </head>
- <body>
- <div class="container" id="container">
- <main class="main-view" id="mainView"></main>
- </div>
- <div class="loading-overlay" id="loadingOverlay" style="display: flex;">
- <div class="loading-spinner"></div>
- <div class="loading-text" data-i18n="loadingText">加载中...</div>
- </div>
- <div class="error-overlay" id="errorOverlay" style="display: none;">
- <div class="error-content">
- <div class="error-text" data-i18n="errorText">加载失败,请稍后重试</div>
- <div class="error-actions">
- <button class="error-retry" id="retryBtn" data-i18n="retryBtn">重试</button>
- <button class="error-retry" id="errorBackBtn" data-i18n="errorBackBtn">返回</button>
- </div>
- </div>
- </div>
- <div class="toast" id="toast"></div>
- <div class="modal-overlay" id="modalOverlay" style="display: none;"></div>
- <script src="/js/common.js"></script>
- <script>
- (function () {
- if (window.BEXTheme) {
- window.BEXTheme.skipAutoInit();
- }
- document.documentElement.classList.add('theme-dark');
- })();
- </script>
- <script>
- (function () {
- var API_BASE = '/api/v1/user/inviteActivityEntry';
- var config = null;
- var activities = [];
- var token = window.WELFARE_TOKEN || '';
- var countdownTimers = {};
- var sliderVolume = 0;
- var T = function (key, params) {
- return window.BEXI18n ? window.BEXI18n.t('inviteActivity', key, params) : key;
- };
- function getDefaultHeaders() {
- return window.BEXI18n ? window.BEXI18n.getDefaultHeaders() : {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json',
- 'X-Requested-With': 'XMLHttpRequest'
- };
- }
- function showLoading() {
- document.getElementById('loadingOverlay').style.display = 'flex';
- hideError();
- }
- function hideLoading() {
- document.getElementById('loadingOverlay').style.display = 'none';
- }
- function showError() {
- document.getElementById('errorOverlay').style.display = 'flex';
- }
- function hideError() {
- document.getElementById('errorOverlay').style.display = 'none';
- }
- function showToast(message) {
- var toast = document.getElementById('toast');
- toast.textContent = message;
- toast.classList.add('show');
- setTimeout(function () { toast.classList.remove('show'); }, 2500);
- }
- function formatAmount(val) {
- var num = parseFloat(val) || 0;
- var str = num.toString();
- var dot = str.indexOf('.');
- if (dot === -1) return str;
- return str.substring(0, dot + 3).replace(/\.?0+$/, '');
- }
- function getNowUtc() {
- return Date.now();
- }
- function fetchConfig() {
- return window.BEXApi.requestWithAuth(API_BASE + '/config', {
- method: 'GET',
- headers: getDefaultHeaders()
- },token);
- }
- function fetchActivityList() {
- return window.BEXApi.requestWithAuth(API_BASE + '/activityList', {
- method: 'GET',
- headers: getDefaultHeaders()
- },token);
- }
- function getInviteLink(obj) {
- var row = { ...obj };
- var params = new URLSearchParams(row).toString();
- return window.location.origin + '/html/invite/invite-register.html?' + params;
- }
- function escapeHtml(text) {
- var div = document.createElement('div');
- div.textContent = text || '';
- return div.innerHTML;
- }
- function copyText(text) {
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(text).then(
- function () { showToast(T('copySuccess')); },
- function () { fallbackCopy(text); }
- );
- } else {
- fallbackCopy(text);
- }
- }
- function fallbackCopy(text) {
- var input = document.createElement('textarea');
- input.value = text;
- input.style.position = 'fixed';
- input.style.opacity = '0';
- document.body.appendChild(input);
- input.select();
- try {
- document.execCommand('copy');
- showToast(T('copySuccess'));
- } catch (e) {
- showToast(T('copyFailed'));
- }
- document.body.removeChild(input);
- }
- function estimateCommission(volumeK) {
- if (!config) return 0;
- var ratio = parseFloat(config.friendTradeVolumeToCommissionRatio) || 0;
- var upper = parseFloat(config.friendTradeVolumeUpperLimitK) || 0;
- var v = Math.min(volumeK, upper);
- return parseInt(v * ratio / 100);
- }
- function stopCountdowns() {
- Object.keys(countdownTimers).forEach(function (id) {
- clearInterval(countdownTimers[id]);
- });
- countdownTimers = {};
- }
- function startActivityCountdown(activity) {
- var id = activity.id;
- if (!activity.endTime) return;
- var endTime = new Date(activity.endTime.replace(' ', 'T')).getTime();
- var daysEl = document.getElementById('activityDays_' + id);
- if (!daysEl) return;
- function tick() {
- var remaining = endTime - getNowUtc();
- var daysEl2 = document.getElementById('activityDays_' + id);
- if (!daysEl2) {
- clearInterval(countdownTimers[id]);
- delete countdownTimers[id];
- return;
- }
- if (remaining <= 0) {
- clearInterval(countdownTimers[id]);
- delete countdownTimers[id];
- daysEl2.textContent = '00';
- var timeEl = document.getElementById('activityTime_' + id);
- if (timeEl) timeEl.textContent = '00 : 00 : 00';
- var btn = document.getElementById('activityBtn_' + id);
- if (btn) {
- btn.disabled = true;
- var btnSpan = btn.querySelector('span');
- if (btnSpan) btnSpan.textContent = T('btnEnded');
- }
- return;
- }
- var days = Math.floor(remaining / 86400000);
- var hours = Math.floor((remaining % 86400000) / 3600000);
- var minutes = Math.floor((remaining % 3600000) / 60000);
- var seconds = Math.floor((remaining % 60000) / 1000);
- var pad = function (n) { return n < 10 ? '0' + n : '' + n; };
- daysEl2.textContent = pad(days);
- var timeEl = document.getElementById('activityTime_' + id);
- if (timeEl) timeEl.textContent = pad(hours) + ' : ' + pad(minutes) + ' : ' + pad(seconds);
- }
- tick();
- countdownTimers[id] = setInterval(tick, 1000);
- }
- function renderPage() {
- var main = document.getElementById('mainView');
- main.innerHTML = '';
- renderHero(main);
- renderCommission(main);
- renderActivityList(main);
- renderInviteLink(main);
- BEXI18n.applyTranslations('inviteActivity');
- }
- function renderHero(container) {
- var title = (config && config.title) ? config.title : T('heroTitle');
- var rate = formatAmount(config && config.rebateRate);
- var duration = (config && config.rebateDurationDays) || 0;
- // var isKyc = !!(config && config.isKycVerified);
- var isKyc = false;
- var btnText = isKyc ? T('activatedBtn') : T('activateBtn');
- var btnDisabled = isKyc ? 'disabled' : '';
- var html =
- '<div class="hero-card">' +
- '<div class="hero-card-inner">' +
- '<div class="hero-bg"></div>' +
- '<div class="hero-title">' + escapeHtml(title) + '</div>' +
- '<div class="hero-subtitle">' + T('heroSubtitle', { rate: rate }) + '</div>' +
- '<div class="hero-rights">' +
- '<div class="hero-rights-line left"></div>' +
- '<span class="hero-rights-text" data-i18n="heroRebateRights">返佣权益</span>' +
- '<div class="hero-rights-line"></div>' +
- '</div>' +
- '<div class="hero-stats">' +
- '<div class="hero-stat">' +
- '<div><span class="hero-stat-value">' + duration + '</span><span class="hero-stat-unit" data-i18n="heroDays">天</span></div>' +
- '<div class="hero-stat-label" data-i18n="heroRebateDuration">返佣时长</div>' +
- '</div>' +
- '<div class="hero-stat">' +
- '<div><span class="hero-stat-value">' + rate + '</span><span class="hero-stat-unit">%</span></div>' +
- '<div class="hero-stat-label" data-i18n="heroRebateRate">返佣比例</div>' +
- '</div>' +
- '</div>' +
- '</div>' +
- '<button class="hero-activate-btn" id="heroActivateBtn" ' + btnDisabled + '>' + btnText + '</button>' +
- '</div>';
- container.insertAdjacentHTML('beforeend', html);
- document.getElementById('heroActivateBtn').onclick = function () {
- // if (isKyc) return;
- showActivateModal();
- };
- }
- function renderCommission(container) {
- var lower = parseFloat(config && config.friendTradeVolumeLowerLimitK) || 0;
- var upper = parseFloat(config && config.friendTradeVolumeUpperLimitK) || 0;
- sliderVolume = lower;
- var initialCommission = estimateCommission(sliderVolume);
- var maxCommission = estimateCommission(upper);
- var html =
- '<div class="commission-section">' +
- '<div class="commission-left">' +
- '<i class="commission-tag-label" data-i18n="commissionLeftLabel">' + T('commissionLeftLabel') + '</i>' +
- '<p class="meny">≈ $<span id="commissionCurrentVal">' + formatAmount(initialCommission) + '</span></p>' +
- '<p class="info" data-i18n="commissionMonthly">' + T('commissionMonthly') + '</p>' +
- '</div>' +
- '<div class="commission-right">' +
- '<i class="commission-tag-label" data-i18n="commissionRightLabel">' + T('commissionRightLabel') + '</i>' +
- '<p class="meny">≈ $<span id="commissionMaxVal">' + formatAmount(maxCommission) + '</span> <span class="info" data-i18n="commissionStarting">' + T('commissionStarting') + '</span></p>' +
- '<p class="info" data-i18n="commissionMonthly">' + T('commissionMonthly') + '</p>' +
- '</div>' +
- '</div>' +
- '<div class="volume-section">' +
- '<div class="volume-slider-wrap">' +
- '<span class="volume-title" data-i18n="friendVolume">' + T('friendVolume') + '</span>' +
- '<span class="volume-tx">' + formatAmount(lower) + 'K</span>' +
- '<div class="volume-box" id="volumeBox">' +
- '<div class="volume-track">' +
- '<div class="volume-progress" id="volumeProgress"></div>' +
- '</div>' +
- '<div class="volume-thumb-box" id="volumeThumb"><i class="volume-thumb" ></i></div>' +
- '<div class="volume-current" id="volumeCurrent">' + formatAmount(lower) + 'K</div>' +
- '</div>' +
- '<span class="volume-tx">' + formatAmount(upper) + 'K</span>' +
- '</div>' +
- '</div>';
- container.insertAdjacentHTML('beforeend', html);
- var volumeBox = document.getElementById('volumeBox');
- var thumb = document.getElementById('volumeThumb');
- var progress = document.getElementById('volumeProgress');
- var currentLabel = document.getElementById('volumeCurrent');
- // var currentValEl = document.getElementById('commissionCurrentVal');
- var maxValEl = document.getElementById('commissionMaxVal');
- function setValueFromPercent(percent) {
- percent = Math.max(0, Math.min(1, percent));
- var raw = lower + percent * (upper - lower);
- return Math.round(raw);
- }
- function updateSlider(clientX) {
- var rect = volumeBox.getBoundingClientRect();
- var x = clientX - rect.left;
- var p = rect.width > 0 ? x / rect.width : 0;
- var val = setValueFromPercent(p);
- sliderVolume = val;
- renderSliderUI(val);
- }
- function renderSliderUI(val) {
- var min = lower;
- var max = upper;
- var rawPercent = max === min ? 0 : (val - min) / (max - min);
- var visualPercent = 0 + rawPercent * 100;
- currentLabel.textContent = formatAmount(val) + 'K';
- currentLabel.style.left = visualPercent + '%';
- thumb.style.left = visualPercent + '%';
- progress.style.width = visualPercent + '%';
- maxValEl.textContent = formatAmount(estimateCommission(val));
- }
- var dragging = false;
- thumb.addEventListener('touchstart', function (e) {
- dragging = true;
- e.preventDefault();
- }, { passive: false });
- thumb.addEventListener('mousedown', function (e) {
- dragging = true;
- e.preventDefault();
- });
- document.addEventListener('touchmove', function (e) {
- if (!dragging) return;
- updateSlider(e.touches[0].clientX);
- }, { passive: false });
- document.addEventListener('mousemove', function (e) {
- if (!dragging) return;
- updateSlider(e.clientX);
- });
- document.addEventListener('touchend', function () { dragging = false; });
- document.addEventListener('mouseup', function () { dragging = false; });
- volumeBox.addEventListener('click', function (e) {
- if (e.target === thumb) return;
- updateSlider(e.clientX);
- });
- renderSliderUI(lower);
- }
- function renderActivityList(container) {
- var html = '<div class="activity-list-section">' +
- '<div class="activity-list-title" data-i18n="activityListTitle">邀请活动</div>';
- if (!activities || activities.length === 0) {
- html += '<div class="activity-empty" data-i18n="activityEmpty">暂无活动</div>';
- html += '</div>';
- container.insertAdjacentHTML('beforeend', html);
- return;
- }
- for (var i = 0; i < activities.length; i++) {
- var act = activities[i];
- var isEnded = !act.endTime || getNowUtc() >= new Date(act.endTime.replace(' ', 'T')).getTime();
- var btnText = isEnded ? T('btnEnded') : T('btnJoin');
- var btnDisabled = isEnded ? 'disabled' : '';
- html +=
- '<div class="activity-card">' +
- '<div class="activity-card-bg"></div>' +
- '<div class="activity-card-image"' + (act.icon ? ' style="background-image: url(' + escapeHtml(act.icon) + ')"' : '') + '></div>' +
- '<div class="activity-card-title">' + escapeHtml(act.activityName || '') + '</div>' +
- '<div class="activity-card-footer">' +
- '<div class="activity-countdown">' +
- '<span class="activity-countdown-label" data-i18n="countdownLabel">距结束</span>' +
- '<span class="activity-countdown-days" id="activityDays_' + act.id + '">00</span>' +
- '<span class="activity-countdown-unit" data-i18n="daysUnit">天</span>' +
- '<span class="activity-countdown-time" id="activityTime_' + act.id + '">00 : 00 : 00</span>' +
- '</div>' +
- '<button class="activity-btn" id="activityBtn_' + act.id + '" ' + btnDisabled + '>' +
- '<span>' + btnText + '</span>' +
- '<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">' +
- '<g clip-path="url(#clip0_13933_579)">' +
- '<path d="M8.79187 5L1.29187 5" stroke="currentColor"/>' +
- '<path d="M6.29188 2.5L8.79187 5L6.29188 7.5" stroke="currentColor"/>' +
- '</g>' +
- '<defs>' +
- '<clipPath id="clip0_13933_579">' +
- '<rect width="10" height="10" fill="currentColor" transform="translate(10 10) rotate(180)"/>' +
- '</clipPath>' +
- '</defs>' +
- '</svg>' +
- '</button>' +
- '</div>' +
- '</div>';
- }
- html += '</div>';
- container.insertAdjacentHTML('beforeend', html);
- for (var j = 0; j < activities.length; j++) {
- (function (act) {
- var btn = document.getElementById('activityBtn_' + act.id);
- if (btn) {
- btn.onclick = function () {
- navigateTo('/html/business/bidirectional-activity.html', { activityId: act.id });
- };
- }
- startActivityCountdown(act);
- })(activities[j]);
- }
- }
- function renderInviteLink(container) {
- var inviteCode = (config && config.inviteCode) || '';
- var link = getInviteLink({
- inviteCode: inviteCode || '',
- });
- var html =
- '<div class="invite-link-section">' +
- '<div class="invite-link-card">' +
- '<div class="invite-link-info">' +
- '<div class="invite-link-label" data-i18n="inviteLinkLabel">邀请链接</div>' +
- '<div class="invite-link-url" id="inviteLinkUrl">' + escapeHtml(link) + '</div>' +
- '</div>' +
- '<div class="invite-link-actions">' +
- '<div class="invite-link-copy-btn" id="inviteLinkCopyBtn" aria-label="' + T('copyBtn') + '">' +
- '<svg viewBox="0 0 18 18" fill="none"><path d="M4.875 4.66185V2.92969C4.875 2.3472 5.3472 1.875 5.92969 1.875H15.0703C15.6528 1.875 16.125 2.3472 16.125 2.92969V12.0703C16.125 12.6528 15.6528 13.125 15.0703 13.125H13.3186" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/><path d="M12.0703 4.875H2.92969C2.3472 4.875 1.875 5.3472 1.875 5.92969V15.0703C1.875 15.6528 2.3472 16.125 2.92969 16.125H12.0703C12.6528 16.125 13.125 15.6528 13.125 15.0703V5.92969C13.125 5.3472 12.6528 4.875 12.0703 4.875Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>' +
- '</div>' +
- '</div>' +
- '</div>' +
- '<div class="invite-link-share-btn" id="inviteLinkShareBtn" aria-label="Share">' +
- // '<svg viewBox="0 0 18 18" fill="none"><circle cx="9" cy="9" r="7.5" stroke="currentColor" stroke-width="1.2"/><circle cx="9" cy="9" r="3" stroke="currentColor" stroke-width="1.2"/></svg>' +
- '<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">' +
- '<path d="M10.5 2.25H15.75V7.5" stroke="currentColor" stroke-width="1.2"/>' +
- '<path d="M15.75 11.0526V14.625C15.75 15.2463 15.2463 15.75 14.625 15.75H3.375C2.75368 15.75 2.25 15.2463 2.25 14.625V3.375C2.25 2.75368 2.75368 2.25 3.375 2.25H6.75" stroke="currentColor" stroke-width="1.2"/>' +
- '<path d="M9.67493 8.32344L15.4124 2.58594" stroke="currentColor" stroke-width="1.2"/>' +
- '</svg>' +
- '</div>' +
- '</div>';
- container.insertAdjacentHTML('beforeend', html);
- document.getElementById('inviteLinkCopyBtn').onclick = function () {
- copyText(getInviteLink({
- inviteCode: config?.inviteCode || '',
- // activityId: config?.relatedInviteActivityId || '',
- }));
- };
- document.getElementById('inviteLinkShareBtn').onclick = jumpToInvite;
- // 底部邀请栏滚动显隐动画
- var inviteSection = document.querySelector('.invite-link-section');
- var scrollThreshold = 80; // 滚动超过80px显示
- var ticking = false;
- function updateInviteSectionVisibility() {
- var docHeight = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight || 0);
- var winHeight = window.innerHeight || document.documentElement.clientHeight || 0;
- var maxScrollTop = docHeight - winHeight;
- // 页面不可滚动,或最大滚动距离不超过阈值时,始终显示
- if (maxScrollTop <= 0 || maxScrollTop <= scrollThreshold) {
- inviteSection.classList.add('visible');
- return;
- }
- var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
- if (scrollTop > scrollThreshold) {
- inviteSection.classList.add('visible');
- } else {
- inviteSection.classList.remove('visible');
- }
- }
- window.addEventListener('scroll', function () {
- if (!ticking) {
- window.requestAnimationFrame(function () {
- updateInviteSectionVisibility();
- ticking = false;
- });
- ticking = true;
- }
- }, { passive: true });
- // 初始检查一次
- updateInviteSectionVisibility();
- }
- function showActivateModal() {
- var overlay = document.getElementById('modalOverlay');
- const ID = config?.relatedInviteActivityId;
- const item = activities.find(item => item.id === ID);
- overlay.className = 'modal-overlay centered';
- overlay.innerHTML =
- '<div class="activate-modal">' +
- '<div class="activate-modal-title" data-i18n="modalActivateTitle">立即激活</div>' +
- '<div class="activate-modal-row" id="activateGoVerifyRow">' +
- '<span class="activate-modal-row-text" data-i18n="completeIdentity">完成身份认证</span>' +
- '<p class="activate-modal-row-action">' +
- '<span data-i18n="goVerify">去认证</span>' +
- '<svg width="14" height="14" viewBox="0 0 18 18" fill="none"><path d="M6.5 4l5 5-5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
- '</span>' +
- '</p>' +
- '</div>' +
- '<button class="activate-modal-btn" id="activateGoInviteBtn" data-i18n="goInvite">去邀请</button>' +
- '<div class="activate-modal-note">' + T('statsNote', { endTime: item?.startTime?.split('T')?.[0]+ ' ' +item?.startTime?.split('T')?.[1] }) + '</div>' +
- '</div>';
- overlay.style.display = 'flex';
- overlay.onclick = function (e) { if (e.target === overlay) closeModal(); };
- BEXI18n.applyTranslations('inviteActivity');
- document.getElementById('activateGoVerifyRow').onclick = jumpToKyc;
- document.getElementById('activateGoInviteBtn').onclick = jumpToInvite;
- }
- function closeModal() {
- var overlay = document.getElementById('modalOverlay');
- overlay.style.display = 'none';
- overlay.innerHTML = '';
- }
- window.closeInviteActivityModal = closeModal;
- function jumpToKyc() {
- window.BEXBridge.goRouterName('/my/detail/identity-verification')
- }
- function jumpToInvite() {
- var activityId = (config && config.relatedInviteActivityId);
- var inviteCode = (config && config.inviteCode);
- window.BEXBridge.showInviteView({ inviteCode, activityId });
- }
- function bindStaticEvents() {
- document.getElementById('retryBtn').onclick = loadData;
- document.getElementById('errorBackBtn').onclick = function () { window.BEXBridge.closeWebview(); };
- }
- function applyTranslations() {
- if (window.BEXI18n) window.BEXI18n.applyTranslations('inviteActivity');
- }
- async function loadData() {
- showLoading();
- stopCountdowns();
- try {
- var results = await Promise.all([
- fetchConfig().catch(function (e) { throw { source: 'config', error: e }; }),
- fetchActivityList().catch(function (e) { throw { source: 'activityList', error: e }; })
- ]);
- config = results[0];
- activities = results[1] || [];
- hideError();
- renderPage();
- } catch (e) {
- console.error(e);
- showError();
- } finally {
- hideLoading();
- }
- }
- async function init() {
- try {
- token = await window.BEXBridge.getToken();
- } catch (error) {
- }
- bindStaticEvents();
- applyTranslations();
- await loadData();
- }
- document.addEventListener('DOMContentLoaded', init);
- })();
- </script>
- </body>
- </html>
|