1(function(){ 2 // åå°å½åèæ¬ <script> ç src 3 const currentScript = document.currentScript; 4 const ver = currentScript ? new URL(currentScript.src).searchParams.get('v') : ''; 5 6 document.addEventListener('DOMContentLoaded', function() { 7 // Configuration 8 const CONFIG = { 9 headerDataUrl: window.location.href.includes('dev.familypro') ? 'https://fronted.familypro.io/familypro-dev/fronted/sections/header.json' : window.location.href.includes('familypro') ? "https://fronted.familypro.io/familypro/fronted/sections/header.json" : '/fronted/sections/header.json', 10 refreshUrl: '/api/v2/auth/refresh', 11 logoutUrl: '/api/v2/auth/logout', 12 userCenter: '/api/user-center/user-center-info', 13 languageCookieName: 'sbox-l', 14 gaUrl: window.location.href.includes('dev.familypro') ? "https://fronted.familypro.io/familypro-dev/fronted/js/gtagIntegration.js" : "https://fronted.familypro.io/familypro/fronted/js/gtagIntegration.js" 15 // languageList: ['en', 'cn', 'fr', 'it', 'br', 'ko', 'jp', 'ru', 'es'] 16 }; 17 // Google Analytics (æ¾å¨æåé¢) 18 const gtagScript = document.createElement('script'); 19 gtagScript.async = true; 20 gtagScript.crossOrigin = "anonymous"; 21 gtagScript.src = CONFIG.gaUrl; 22 document.head.appendChild(gtagScript); 23 24 window.$crisp = []; 25 window.CRISP_WEBSITE_ID = "af499a23-ba2f-48aa-b55b-d3d5608d5ef5"; 26 (function() { 27 const d = document; 28 const s = d.createElement("script"); 29 s.src = "https://client.crisp.chat/l.js"; 30 s.crossOrigin = "anonymous"; 31 s.async = true; 32 d.getElementsByTagName("head")[0].appendChild(s); 33 })(); 34 35 // æ ¹æ®å¾çä¸çæ©è²ä¸»é¢é ç½®Crisp 36 window.$crisp.push(['set', 'theme:color', '#FF6B00']); // 使ç¨å¾çä¸çæ©è²ä¸»é¢è² 37 // 设置 Crisp è天çªå£é»è®¤ä¸æå¼ 38 window.$crisp.push(['do', 'chat:close']); 39 40 // æ·»å Google Site Verification meta æ ç¾ 41 const googleVerificationMeta = document.createElement('meta'); 42 googleVerificationMeta.name = 'google-site-verification'; 43 googleVerificationMeta.content = 'y72P5EzUWOM0cyrWfkxxrM41KhIPoOyZp9IHCRao-1A'; 44 document.head.appendChild(googleVerificationMeta); 45 46 let languageList = ['en', 'cn', 'fr', 'it', 'br', 'ko', 'jp', 'ru', 'es', 'de'] 47 const SELLER_NAV_TEXT = { 48 en: 'BE SELLER', 49 cn: 'æä¸ºåå®¶', 50 fr: 'Devenir vendeur', 51 it: 'Diventa venditore', 52 br: 'Torne-se vendedor', 53 ko: 'í매ì ë기', 54 jp: 'åºåè ã«ãªã', 55 ru: 'СÑаÑÑ Ð¿ÑодавÑом', 56 es: 'Conviértete en vendedor', 57 de: 'Verkäufer werden' 58 } 59 const AFFILIATE_NAV_TEXT = { 60 en: 'AFFILIATE', 61 cn: 'èç计å', 62 fr: 'Affiliation', 63 it: 'Affiliati', 64 br: 'Afiliados', 65 ko: 'ì í´ íë¡ê·¸ë¨', 66 jp: 'ã¢ãã£ãªã¨ã¤ã', 67 ru: 'ÐаÑÑнÑÑÑÐºÐ°Ñ Ð¿ÑогÑамма', 68 es: 'Afiliados', 69 de: 'Partnerprogramm' 70 } 71 const AFFILIATE_OVERVIEW_TEXT = { 72 en: 'Program Overview', 73 cn: 'è®¡åæ¦è§', 74 fr: 'Vue dâensemble du programme', 75 it: 'Panoramica del programma', 76 br: 'Visão geral do programa', 77 ko: 'íë¡ê·¸ë¨ ê°ì', 78 jp: 'ããã°ã©ã æ¦è¦', 79 ru: 'ÐÐ±Ð·Ð¾Ñ Ð¿ÑогÑаммÑ', 80 es: 'Resumen del programa', 81 de: 'Programmübersicht' 82 } 83 const AFFILIATE_CENTER_TEXT = { 84 en: 'Affiliate Center', 85 cn: 'èçä¸å¿', 86 fr: 'Espace affiliation', 87 it: 'Centro affiliati', 88 br: 'Central de afiliados', 89 ko: 'ì í´ ì¼í°', 90 jp: 'ã¢ãã£ãªã¨ã¤ãã»ã³ã¿ã¼', 91 ru: 'ÐаÑÑнÑÑÑкий ÑенÑÑ', 92 es: 'Centro de afiliados', 93 de: 'Partnerbereich' 94 } 95 const QUICK_SHARE_PENDING_KEY = 'fp_pending_quick_share'; 96 const PROMO_CODE_STORAGE_KEY = 'fp_promo_code'; 97 let isPromoValidated = false; 98 99 // Global variables 100 let dataResource = {}; 101 let currentLanguage = 'en'; 102 let isAuthenticated = false; 103 let quickShareCache = null; 104 let quickShareOverlay = null; 105 106 // ==================== Initialization ==================== 107 async function initializeHeader() {
108 try { 109 crispReset(); 110 initCurrentLanguage(); 111 await fetchHeaderData(); 112 currentLanguage = getcurrentLanguage(); 113 buildHeader(); 114 await initAnnouncementBar(); 115 buildQuickShareButton(); 116 buildQuickFeedbackButton(); 117 loadFavicon(); 118 await initPromoFromUrlAndValidate(); 119 await checkAuthState(); 120 await redeemPromoIfNeeded(); 121 maybeShowPromoDialogForGuest(); 122 await maybeOpenPendingQuickShare(); 123 translateHeader(currentLanguage); 124 setupEventListeners(); 125 window.addEventListener('storage', handleStorageChange); 126 } catch (error) { 127 console.error('Header initialization failed:', error); 128 } finally { 129 // åªè¦æé请ç 就添å 䏿¬¡è®¿é®è®°å½ 130 const urlParams = new URLSearchParams(window.location.search); 131 const inviteCode = urlParams.get('invite'); 132 if (inviteCode != null) { 133 reportLog('invite_code_url', 'visit', { 134 'inviteCode': inviteCode 135 }) 136 } 137 138 const promoCode = getPromoCodeFromUrl() 139 if (promoCode) { 140 reportLog('Promo_Redemption_Start', 'KOL_Promo', { 141 source: promoCode, 142 url: window.location.href, 143 }) 144 } 145 } 146 } 147 148 async function crispReset() { 149 window.$crisp.push(["do", "session:reset"]); 150 } 151 152 // æ°å¢Faviconå è½½å½æ°ï¼æ¾å¨æä»¶æ«å°¾çinitializeHeaderä¹åï¼ 153 function loadFavicon() { 154 // æ ¹æ®å¾çä¸ç绿è²V形徿 飿 ¼é ç½® 155 const faviconUrl = 'https://fronted.familypro.io/familypro/fronted/resource/logo/familypro.png'; 156 157 // é¿å éå¤å建 158 let link = document.querySelector("link[rel~='icon']"); 159 160 if (!link) { 161 link = document.createElement('link'); 162 link.rel = 'icon'; 163 document.head.appendChild(link); 164 } 165
166 // 强å¶å·æ°ç¼åï¼é对å¾çä¸å¯è½éå°çç¼åé®é¢ï¼ 167 link.href = `${faviconUrl}?t=${new Date().getTime()}`; 168 link.type = 'image/png'; 169 170 // åæ¶è®¾ç½®Apple Touch Iconï¼ç§»å¨ç«¯éé ï¼ 171 const appleLink = document.querySelector("link[rel~='apple-touch-icon']") || 172 document.createElement('link'); 173 appleLink.rel = 'apple-touch-icon'; 174 appleLink.href = faviconUrl; 175 document.head.appendChild(appleLink); 176 } 177 178 let isCheckingAuth = false; 179 function handleStorageChange(event) { 180 if (event.key === 'token' && event.newValue === null) { 181 isAuthenticated = false; 182 quickShareCache = null; 183 closeQuickShareModal(); 184 return; 185 } 186 if (event.key === 'token' && event.oldValue === null && event.newValue !== null && !isCheckingAuth) { 187 isCheckingAuth = true; 188 checkAuthState().then(() => { 189 translateHeader(currentLanguage); 190 isCheckingAuth = false; 191 }).catch(() => { 192 isCheckingAuth = false; 193 }); 194 } 195 } 196 197 // ==================== Data Fetching ==================== 198 async function fetchHeaderData() { 199 try { 200 const response = await fetch(CONFIG.headerDataUrl + `?v=${ver}`); 201 if (!response.ok) { 202 throw new Error(`HTTP error! status: ${response.status}`); 203 } 204 dataResource = await response.json(); 205 } catch (error) { 206 console.error('Failed to fetch header data:', error); 207 throw error; 208 } 209 } 210 211 // ==================== System Announcement Bar ==================== 212 const ANNOUNCEMENT_CAROUSEL_MS = 6000; 213 214 const ANNOUNCEMENT_TYPE_CONFIG = { 215 info: { 216 icon: '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 10v6M12 7h.01"/></svg>', 217 barClass: 'fp-announcement-bar--info' 218 }, 219 update: { 220 icon: '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v4M12 17v4M4.2 7.2l2.8 2.8M17 14l2.8 2.8M3 12h4M17 12h4M7.2 19.8l2.8-2.8M17 10l2.8-2.8"/></svg>', 221 barClass: 'fp-announcement-bar--update' 222 }, 223 activity: { 224 icon: '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l2.4 6.8H21l-5.5 4.1 2.1 6.7L12 16.4 6.4 20.6l2.1-6.7L3 9.8h6.6L12 3z"/></svg>', 225 barClass: 'fp-announcement-bar--activity' 226 }, 227 stock: { 228 icon: '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><path d="M3.3 7.3L12 12l8.7-4.7M12 22V12"/></svg>', 229 barClass: 'fp-announcement-bar--stock' 230 }, 231 warning: { 232 icon: '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4M12 17h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg>', 233 barClass: 'fp-announcement-bar--warning' 234 }, 235 danger: { 236 icon: '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 8v5M12 16h.01"/><path d="M10.07 2.82l-8.5 14.74A2 2 0 003.26 20h17.48a2 2 0 001.69-2.44l-8.5-14.74a2 2 0 00-3.38 0z"/></svg>', 237 barClass: 'fp-announcement-bar--danger' 238 }, 239 maintenance: { 240 icon: '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/></svg>', 241 barClass: 'fp-announcement-bar--maintenance' 242 } 243 }; 244 245 const ANNOUNCEMENT_I18N = { 246 en: { popupTitle: 'Announcement', close: 'Close' }, 247 cn: { popupTitle: 'å ¬å详æ ', close: 'å ³é' }, 248 jp: { popupTitle: 'ãç¥ãã', close: 'éãã' }, 249 ko: { popupTitle: 'ê³µì§', close: 'ë«ê¸°' }, 250 fr: { popupTitle: 'Annonce', close: 'Fermer' }, 251 de: { popupTitle: 'Ankündigung', close: 'SchlieÃen' }, 252 es: { popupTitle: 'Anuncio', close: 'Cerrar' }, 253 it: { popupTitle: 'Annuncio', close: 'Chiudi' }, 254 br: { popupTitle: 'Anúncio', close: 'Fechar' }, 255 ru: { popupTitle: 'ÐбÑÑвление', close: 'ÐакÑÑÑÑ' } 256 }; 257 258 const ANNOUNCEMENT_CTA_I18N = { 259 en: { learnMore: 'View details' }, 260 cn: { learnMore: 'æ¥ç详æ ' }, 261 jp: { learnMore: '詳細ãè¦ã' }, 262 ko: { learnMore: 'ìì¸í 보기' }, 263 fr: { learnMore: 'Voir les détails' }, 264 de: { learnMore: 'Details ansehen' }, 265 es: { learnMore: 'Ver detalles' }, 266 it: { learnMore: 'Vedi dettagli' }, 267 br: { learnMore: 'Ver detalhes' }, 268 ru: { learnMore: 'ÐодÑобнее' } 269 }; 270 271 let announcementItems = []; 272 let announcementCarouselTimer = null; 273 let announcementCountdownTimer = null; 274 let announcementIndex = 0; 275 let announcementBarEl = null; 276 277 function mapLanguageForAnnouncementApi(lang) { 278 const map = { cn: 'zh', jp: 'ja', br: 'pt' }; 279 return map[lang] || lang || 'en'; 280 } 281 282 function getAnnouncementI18n(lang) { 283 return ANNOUNCEMENT_I18N[lang] || ANNOUNCEMENT_I18N.en; 284 } 285 286 function getAnnouncementCtaLabel(item, lang) { 287 const labels = ANNOUNCEMENT_CTA_I18N[lang] || ANNOUNCEMENT_CTA_I18N.en; 288 return labels.learnMore; 289 } 290 291 function getAnnouncementDisplayText(item) { 292 const summary = (item.summary || '').trim(); 293 const title = (item.title || '').trim(); 294 return summary || title || ''; 295 } 296 297 function resolveAnnouncementType(item) { 298 const type = item && item.announcement_type; 299 if (type && ANNOUNCEMENT_TYPE_CONFIG[type]) return type; 300 const levelMap = { 4: 'danger', 3: 'warning', 2: 'update', 1: 'info' }; 301 return levelMap[item && item.level] || 'info'; 302 } 303 304 function getAnnouncementTypeConfig(item) { 305 const type = resolveAnnouncementType(item); 306 return ANNOUNCEMENT_TYPE_CONFIG[type] || ANNOUNCEMENT_TYPE_CONFIG.info; 307 } 308 309 /** æ æ¶åºæ¶æ UTC è§£æï¼ä¸ Date.now() æ±å·®ï¼ä¸ src/utils/announcementCountdown.js ä¸è´ï¼ */ 310 function parseCountdownEndUtcMs(endTimeStr) { 311 if (endTimeStr == null || endTimeStr === '') return NaN; 312 var raw = String(endTimeStr).trim(); 313 if (!raw) return NaN; 314 var normalized = raw.indexOf('T') >= 0 ? raw : raw.replace(' ', 'T'); 315 var hasTz = /(?:[zZ]|[+-]\d{2}:?\d{2})$/.test(normalized); 316 var iso = hasTz ? normalized : normalized + 'Z'; 317 var t = Date.parse(iso); 318 return Number.isFinite(t) ? t : NaN; 319 } 320 321 function getCountdownParts(endTimeStr) { 322 const end = parseCountdownEndUtcMs(endTimeStr); 323 if (!Number.isFinite(end)) return { h: 0, m: 0, s: 0, expired: true }; 324 const now = Date.now(); 325 if (end <= now) return { h: 0, m: 0, s: 0, expired: true }; 326 let diff = end - now; 327 const h = Math.floor(diff / 3600000); 328 diff -= h * 3600000; 329 const m = Math.floor(diff / 60000); 330 diff -= m * 60000; 331 const s = Math.floor(diff / 1000); 332 return { h, m, s, expired: false }; 333 } 334 335 function renderTimerDigitsHtml(parts) { 336 const pad = n => String(n).padStart(2, '0'); 337 const box = v => `<span class="fp-announcement-timer-box"><span class="fp-announcement-timer-digit">${pad(v)}</span></span>`;
338 return box(parts.h) + '<span class="fp-announcement-timer-sep">:</span>' + 339 box(parts.m) + '<span class="fp-announcement-timer-sep">:</span>' + box(parts.s); 340 } 341 342 const ANNOUNCEMENT_CLOCK_ICON = 343 '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">' + 344 '<circle cx="12" cy="13" r="8"/>' + 345 '<path d="M12 9v4l2.5 1.5"/>' + 346 '<path d="M9 3h6M12 3v2"/>' + 347 '</svg>'; 348 349 function renderCountdownHtml(item) { 350 if (!item.enable_countdown || !item.countdown_end_time) return ''; 351 const parts = getCountdownParts(item.countdown_end_time); 352 if (parts.expired) return ''; 353 return ( 354 `<div class="fp-announcement-countdown-wrap" data-countdown-end="${item.countdown_end_time}">` + 355 `<div class="fp-announcement-countdown">` + 356 `<span class="fp-announcement-clock-icon" aria-hidden="true">${ANNOUNCEMENT_CLOCK_ICON}</span>` + 357 `<div class="fp-announcement-timer">${renderTimerDigitsHtml(parts)}</div>` + 358 `</div>` + 359 `<span class="fp-announcement-divider" aria-hidden="true"></span>` + 360 `</div>` 361 ); 362 } 363 364 function updateCountdownElements() { 365 if (!announcementBarEl) return; 366 announcementBarEl.querySelectorAll('[data-countdown-end]').forEach(el => { 367 const end = el.getAttribute('data-countdown-end'); 368 const parts = getCountdownParts(end); 369 const timer = el.querySelector('.fp-announcement-timer'); 370 if (parts.expired || !timer) { 371 el.style.display = 'none'; 372 return; 373 } 374 el.style.display = ''; 375 timer.innerHTML = renderTimerDigitsHtml(parts); 376 }); 377 } 378 379 function stopAnnouncementTimers() { 380 if (announcementCarouselTimer) { 381 clearInterval(announcementCarouselTimer); 382 announcementCarouselTimer = null; 383 } 384 if (announcementCountdownTimer) { 385 clearInterval(announcementCountdownTimer); 386 announcementCountdownTimer = null; 387 } 388 } 389 390 function showAnnouncementSlide(index) { 391 if (!announcementBarEl || !announcementItems.length) return; 392 announcementIndex = ((index % announcementItems.length) + announcementItems.length) % announcementItems.length; 393 const item = announcementItems[announcementIndex]; 394 const lang = getcurrentLanguage(); 395 const i18n = getAnnouncementI18n(lang); 396 const typeKey = resolveAnnouncementType(item); 397 const typeConfig = getAnnouncementTypeConfig(item); 398 const text = getAnnouncementDisplayText(item); 399 const countdownHtml = renderCountdownHtml(item); 400 const clickable = item.click_action && item.click_action !== 'none'; 401 const slide = announcementBarEl.querySelector('.fp-announcement-slide'); 402 if (!slide) return; 403 404 announcementBarEl.className = 'fp-announcement-bar ' + (typeConfig.barClass || 'fp-announcement-bar--info'); 405 406 slide.classList.remove('fp-announcement-slide--in'); 407 void slide.offsetWidth; 408 slide.innerHTML = 409 `<div class="fp-announcement-content fp-announcement-type--${typeKey}` + 410 (clickable ? ' fp-announcement-content--link' : '') + `"` + 411 (clickable ? ` data-announcement-id="${item.id}" role="button" tabindex="0"` : '') + `>` + 412 countdownHtml + 413 `<span class="fp-announcement-icon fp-announcement-icon--${typeKey}" aria-hidden="true">${typeConfig.icon}</span>` + 414 `<div class="fp-announcement-main">` + 415 `<span class="fp-announcement-text">${escapeHtml(text)}</span>` + 416 `</div>` + 417 (clickable 418 ? `<span class="fp-announcement-cta" aria-hidden="true">` + 419 `<span class="fp-announcement-cta__label">${escapeHtml(getAnnouncementCtaLabel(item, lang))}</span>` + 420 `<span class="fp-announcement-cta__arrow">â</span></span>` 421 : '') + 422 `</div>`; 423 slide.classList.add('fp-announcement-slide--in'); 424 updateCountdownElements(); 425 } 426 427 function escapeHtml(str) { 428 return String(str) 429 .replace(/&/g, '&') 430 .replace(/</g, '<') 431 .replace(/>/g, '>') 432 .replace(/"/g, '"'); 433 } 434 435 function logViewAnnouncementDetail(item, source) { 436 if (!item || !source) return; 437 reportLog('view_announcement_detail', 'announcement', { 438 source: source, 439 text: String(item.title || item.summary || '').trim(), 440 resource: String(item.announcement_no || item.id || '').trim() 441 }); 442 } 443 444 function openAnnouncementPopup(item) { 445 window.dispatchEvent( 446 new CustomEvent('fp-open-announcement-popup', { 447 detail: { announcement: item, source: 'banner' } 448 }) 449 ); 450 } 451 452 function handleAnnouncementClick(item) { 453 if (!item || item.click_action === 'none') return; 454 if (item.click_action === 'link' && item.link_url) { 455 logViewAnnouncementDetail(item, 'banner'); 456 const url = item.link_url; 457 if (/^https?:\/\//i.test(url)) { 458 window.open(url, '_blank', 'noopener'); 459 } else { 460 window.location.href = url.startsWith('/') ? url : `/${url}`; 461 } 462 return; 463 } 464 if (item.click_action === 'popup') { 465 openAnnouncementPopup(item); 466 } 467 } 468 469 function bindAnnouncementBarEvents() { 470 if (!announcementBarEl) return; 471 announcementBarEl.addEventListener('click', e => { 472 const target = e.target.closest('.fp-announcement-content--link'); 473 if (!target) return; 474 const id = target.getAttribute('data-announcement-id'); 475 const item = announcementItems.find(a => String(a.id) === String(id)); 476 if (item) handleAnnouncementClick(item); 477 }); 478 announcementBarEl.addEventListener('keydown', e => { 479 if (e.key !== 'Enter' && e.key !== ' ') return; 480 const target = e.target.closest('.fp-announcement-content--link'); 481 if (!target) return; 482 e.preventDefault(); 483 const id = target.getAttribute('data-announcement-id'); 484 const item = announcementItems.find(a => String(a.id) === String(id)); 485 if (item) handleAnnouncementClick(item); 486 }); 487 } 488 489 function startAnnouncementCarousel() { 490 stopAnnouncementTimers(); 491 if (announcementItems.length <= 1) return; 492 announcementCarouselTimer = setInterval(() => { 493 showAnnouncementSlide(announcementIndex + 1); 494 }, ANNOUNCEMENT_CAROUSEL_MS); 495 } 496 497 function buildAnnouncementBar() { 498 if (announcementBarEl) { 499 announcementBarEl.remove(); 500 announcementBarEl = null; 501 } 502 if (!announcementItems.length) return; 503 504 const bar = document.createElement('div'); 505 bar.className = 'fp-announcement-bar'; 506 bar.setAttribute('role', 'region'); 507 bar.setAttribute('aria-label', 'system announcement'); 508 bar.innerHTML = '<div class="fp-announcement-inner"><div class="fp-announcement-slide"></div></div>'; 509 announcementBarEl = bar; 510 511 const headerWrap = document.querySelector('.fp-header-wrap'); 512 const header = document.querySelector('.fp-header'); 513 if (headerWrap && header) { 514 headerWrap.classList.add('fp-has-announcement'); 515 if (header.nextSibling) { 516 headerWrap.insertBefore(bar, header.nextSibling); 517 } else { 518 headerWrap.appendChild(bar); 519 } 520 } else if (header) { 521 header.insertAdjacentElement('afterend', bar); 522 } else { 523 document.body.insertBefore(bar, document.body.firstChild); 524 } 525 526 bindAnnouncementBarEvents(); 527 showAnnouncementSlide(0); 528 startAnnouncementCarousel(); 529 announcementCountdownTimer = setInterval(updateCountdownElements, 1000); 530 } 531 532 async function fetchBannerAnnouncements() { 533 const lang = mapLanguageForAnnouncementApi(getcurrentLanguage()); 534 const url = `/api/announcements?display_position=banner&language=${encodeURIComponent(lang)}&limit=20`;
535 try { 536 const resp = await fetch(url, { credentials: 'same-origin' }); 537 if (!resp.ok) return []; 538 const json = await resp.json(); 539 if (json.code !== 100000) return []; 540 const data = json.data; 541 return Array.isArray(data) ? data : (data?.list || data?.items || []); 542 } catch (err) { 543 console.warn('fetch banner announcements failed', err); 544 return []; 545 } 546 } 547 548 async function initAnnouncementBar() { 549 stopAnnouncementTimers(); 550 announcementItems = await fetchBannerAnnouncements(); 551 if (!announcementItems.length) { 552 const headerWrap = document.querySelector('.fp-header-wrap'); 553 if (headerWrap) { 554 headerWrap.classList.remove('fp-has-announcement'); 555 headerWrap.style.height = ''; 556 } 557 return; 558 } 559 buildAnnouncementBar(); 560 } 561 562 // ==================== Header Construction ==================== 563 function buildHeader() { 564 const header = document.createElement('header'); 565 header.className = 'fp-header'; 566 567 const container = document.createElement('div'); 568 container.className = 'fp-container'; 569 570 const headerContent = document.createElement('div'); 571 headerContent.className = 'fp-header-content'; 572 573 // Logo 574 const logoDiv = document.createElement('div'); 575 logoDiv.className = 'fp-logo'; 576 const logoLink = document.createElement('a'); 577 logoLink.className = 'fp-logo-link'; 578 logoLink.href = dataResource.i18nResources[currentLanguage]?.nav?.mart?.url; 579 logoLink.target = '_blank'; 580 const logoImg = document.createElement('img'); 581 logoImg.className = 'fp-logo-img'; 582 logoImg.src = 'https://fronted.familypro.io/familypro/resource/img/logo.png'; 583 logoImg.alt = 'Logo'; 584 logoLink.appendChild(logoImg); 585 logoDiv.appendChild(logoLink); 586 587 // Navigation menu 588 const navMenu = document.createElement('nav'); 589 navMenu.className = 'fp-nav-menu'; 590 const navList = document.createElement('ul'); 591 navList.className = 'fp-nav-list'; 592 593 // Language selector 594 const languageLi = document.createElement('li'); 595 languageLi.className = 'fp-nav-item fp-language-item'; 596 const languageDiv = document.createElement('div'); 597 languageDiv.className = 'fp-language-switcher'; 598 const select = document.createElement('select'); 599 select.id = 'fp-languageSelect'; 600 select.className = 'fp-language-select'; 601 602 const urlLanguage = dataResource.urlLanguage; 603 const url = window.location.pathname 604 let newlagnuageList = findLanguagesForPath(url, urlLanguage) 605 if (newlagnuageList) { 606 languageList = newlagnuageList 607 } 608 609 const languageNavItem = { 610 text: '', 611 items: { 612 613 } 614 } 615 languageList.forEach(langCode => { 616 languageNavItem.items[langCode] = { 617 text: dataResource.i18nResources[langCode]?.nav?.languageName || 618 dataResource.i18nResources[langCode]?.languageName || 619 langCode.toUpperCase(), 620 url: langCode 621 } 622 }); 623 624 languageDiv.appendChild(select); 625 languageLi.appendChild(languageDiv); 626 627 // Add nav items from JSON 628 const navItems = dataResource.i18nResources[currentLanguage]?.nav || dataResource.i18nResources.en.nav; 629 console.log('navItems', navItems) 630 Object.entries(navItems).forEach(([key, item]) => { 631 if (key != 'languageName') { 632 if (key === 'services') { 633 navList.appendChild(createNavItemFromJSON(item, `nav.${key}`)); 634 const affiliateGuestNav = createNavItemFromJSON({ 635 text: AFFILIATE_NAV_TEXT[currentLanguage] || AFFILIATE_NAV_TEXT.en, 636 url: `/${currentLanguage}/affiliate`, 637 auth: false 638 }, 'nav.affiliateGuest', true); 639 affiliateGuestNav.classList.add('fp-affiliate-item', 'fp-affiliate-guest-item'); 640 navList.appendChild(affiliateGuestNav); 641 const affiliateMemberNav = createNavItemFromJSON({ 642 text: AFFILIATE_NAV_TEXT[currentLanguage] || AFFILIATE_NAV_TEXT.en, 643 auth: true, 644 items: { 645 overview: { 646 text: AFFILIATE_OVERVIEW_TEXT[currentLanguage] || AFFILIATE_OVERVIEW_TEXT.en, 647 url: `/${currentLanguage}/affiliate` 648 }, 649 center: { 650 text: AFFILIATE_CENTER_TEXT[currentLanguage] || AFFILIATE_CENTER_TEXT.en, 651 url: '/invite' 652 } 653 } 654 }, 'nav.affiliateMember', true); 655 affiliateMemberNav.classList.add('fp-affiliate-item', 'fp-affiliate-member-item'); 656 navList.appendChild(affiliateMemberNav); 657 const sellerNav = createNavItemFromJSON({ 658 text: SELLER_NAV_TEXT[currentLanguage] || SELLER_NAV_TEXT.en, 659 url: `/${currentLanguage}/sell` 660 }, 'nav.seller', true); 661 sellerNav.classList.add('fp-seller-item'); 662 navList.appendChild(sellerNav); 663 const langNav = createNavItemFromJSON(languageNavItem, `nav.language`, true); 664 langNav.classList.add('fp-language-item'); 665 langNav.querySelector('.fp-dropdown-toggle').textContent = languageNavItem.items[currentLanguage].text; 666 navList.appendChild(langNav); 667 } else if (key !== 'language' && key !== 'affiliate') { 668 navList.appendChild(createNavItemFromJSON(item, `nav.${key}`)); 669 } 670 } 671 }); 672 673 navMenu.appendChild(navList); 674 675 // Mobile menu button 676 const mobileMenuBtn = document.createElement('button'); 677 mobileMenuBtn.className = 'fp-mobile-menu-btn'; 678 mobileMenuBtn.setAttribute('aria-label', 'Toggle navigation menu'); 679 for (let i = 0; i < 3; i++) { 680 mobileMenuBtn.appendChild(document.createElement('span')); 681 } 682 683 // Build structure 684 headerContent.appendChild(logoDiv); 685 headerContent.appendChild(navMenu); 686 headerContent.appendChild(mobileMenuBtn); 687 container.appendChild(headerContent); 688 header.appendChild(container); 689 const headerWrap = document.querySelector('.fp-header-wrap') 690 if (headerWrap) { 691 headerWrap.appendChild(header) 692 } else { 693 document.body.insertBefore(header, document.body.firstChild); 694 } 695 } 696 697 function findLanguagesForPath(path, config) { 698 const pathSegments = path.split('/').filter(seg => seg !== ''); 699 700 // å°è¯å¹é 带è¯è¨åç¼çè·¯å¾ (å¦ /ko/support) 701 if (pathSegments.length >= 2) { 702 const potentialLang = pathSegments[0]; 703 const basePath = pathSegments.slice(1).join('/'); 704 // å¹é ä¼å çº§ï¼æ ¹æ®æ¨çéæ±å®å¶ï¼ 705 const matchAttempts = [ 706 `/${basePath}`, // å ¨è·¯å¾ (å¦ /products/chatgpt) 707 `/${basePath.split('/')[0]}`, // ä» ç¬¬ä¸æ®µ (å¦ /products) 708 `/${basePath.split('/').pop()}` // æå䏿®µ (å¦ /chatgpt) 709 ]; 710 for (const attempt of matchAttempts) { 711 if (config[attempt]) { 712 return config[attempt]; 713 } 714 } 715 } 716 717 // å°è¯å¹é ä¸å¸¦è¯è¨åç¼çè·¯å¾ (å¦ /support) 718 const basePath = '/' + pathSegments.join('/'); 719 if (config[basePath]) { 720 return config[basePath]; 721 } 722 723 // é»è®¤è¿åè±è¯ 724 return null; 725 } 726 727 function createNavItemFromJSON(item, path = 'nav', not_i18n) { 728 const li = document.createElement('li'); 729 li.className = 'fp-nav-item'; 730 731 if (item.auth !== undefined) { 732 li.setAttribute('data-auth', item.auth); 733 li.classList.add('fp-auth-item'); 734 if (item.auth === true) { 735 li.style.display = 'none'; 736 } 737 } 738 739 // æ·»å ç¨æ·èåç¹æ®æ è¯ 740 if (path.includes('userMenu')) { 741 li.classList.add('fp-user-menu-item'); 742 } 743 744 if (item.items) { 745 const dropdownToggle = document.createElement('a'); 746 dropdownToggle.target = "_blank"; 747 dropdownToggle.className = 'fp-dropdown-toggle'; 748 dropdownToggle.textContent = item.text; 749 !not_i18n && dropdownToggle.setAttribute('data-i18n', `${path}.text`); 750 dropdownToggle.setAttribute('data-url-key', path); 751 752 if (item.auth !== undefined) { 753 dropdownToggle.setAttribute('data-auth', item.auth); 754 } 755 756 // 为userMenuæ·»å ç¹æ®å¤ç 757 if (path.includes('userMenu')) { 758 dropdownToggle.textContent = ''; 759 const emailSpan = document.createElement('span'); 760 emailSpan.className = 'fp-user-email'; 761 // åå§ææ¬è®¾ä¸ºç©ºï¼ç±checkAuthStateå¡«å 762 emailSpan.textContent = ''; 763 dropdownToggle.insertBefore(emailSpan, dropdownToggle.firstChild); 764 } 765 766 const dropdownMenu = document.createElement('div'); 767 dropdownMenu.className = item.type === 'group' ? 'fp-dropdown-menu fp-dropdown-menu-grouped': 'fp-dropdown-menu';
768 Object.entries(item.items).forEach(([key, subItem]) => { 769 const subPath = `${path}.items.${key}`; 770 if (subItem.items) { 771 const submenuContainer = document.createElement('div'); 772 submenuContainer.className = 'fp-dropdown-submenu'; 773 774 const submenuToggle = document.createElement('div'); 775 submenuToggle.className = 'fp-dropdown-group-title'; 776 submenuToggle.textContent = subItem.text; 777 !not_i18n && submenuToggle.setAttribute('data-i18n', `${subPath}.text`); 778 submenuToggle.setAttribute('data-url-key', subPath); 779 780 const submenuMenu = document.createElement('div'); 781 submenuMenu.className = 'fp-dropdown-submenu-group'; 782 783 Object.entries(subItem.items).forEach(([subKey, subSubItem]) => { 784 const subSubPath = `${subPath}.items.${subKey}`; 785 const submenuItem = createMenuItem(subSubItem, subSubPath); 786 submenuMenu.appendChild(submenuItem); 787 }); 788 789 submenuContainer.appendChild(submenuToggle); 790 submenuContainer.appendChild(submenuMenu); 791 dropdownMenu.appendChild(submenuContainer); 792 } else { 793 const dropdownItem = createMenuItem(subItem, subPath, not_i18n); 794 dropdownMenu.appendChild(dropdownItem); 795 } 796 }); 797 798 li.appendChild(dropdownToggle); 799 li.appendChild(dropdownMenu); 800 } else { 801 const menuItem = createMenuItem(item, path, not_i18n); 802 li.appendChild(menuItem); 803 } 804 805 return li; 806 } 807 808 function createMenuItem(item, path, not_i18n) { 809 810 let a = null; 811 if (path.includes("nav.language")) { 812 a = document.createElement('span'); 813 a.setAttribute("url-data", item.url || "#"); 814 } else { 815 if (item.url.includes("logout")) { 816 a = document.createElement('span'); 817 a.setAttribute("url-data", item.url || "#"); 818 // set id fp-logoutBtn 819 a.id = "fp-logoutBtn"; 820 } else { 821 a = document.createElement('a'); 822 a.target = "_blank"; 823 a.href = item.url || '#'; 824 } 825 826 } 827 828 a.setAttribute('data-url-key', path); 829 a.className = 'fp-dropdown-item'; 830 831 if (!not_i18n) { 832 const textSpan = document.createElement('span'); 833 textSpan.setAttribute('data-i18n', `${path}.text`); 834 textSpan.textContent = item.text; 835 a.append(textSpan); 836 837 if (item.status === 'hot') { 838 const textSpan = document.createElement('span'); 839 textSpan.textContent = 'HOT'; 840 textSpan.className = 'fp-dropdown-item-status item-hot'; 841 a.append(textSpan); 842 } else if (item.status === 'new') { 843 const textSpan = document.createElement('span'); 844 textSpan.textContent = 'NEW'; 845 textSpan.className = 'fp-dropdown-item-status item-new'; 846 a.append(textSpan); 847 } 848 } else { 849 a.textContent = item.text; 850 } 851 852 if (item.auth !== undefined) { 853 a.setAttribute('data-auth', item.auth); 854 } 855 856 return a; 857 } 858 859 // ==================== Language Functions ==================== 860 function initCurrentLanguage() { 861 let path = window.location.pathname; 862 const pathSegments = path.split('/').filter(seg => seg !== ''); 863 if (pathSegments.length > 0 && languageList.includes(pathSegments[0])) { 864 setCookie('sbox-l', pathSegments[0]); 865 return pathSegments[0]; 866 } else { 867 return getCookie("sbox-l") || "en"; 868 } 869 } 870 871 function getcurrentLanguage() { 872 let path = window.location.pathname; 873 if (path === '/') { 874 return "en"; 875 } 876 const pathSegments = path.split('/').filter(seg => seg !== ''); 877 let newLang = findLanguagesForPath(path, dataResource.urlLanguage); 878 if (newLang) { 879 languageList = newLang; 880 } 881 if (pathSegments.length > 0 && languageList.includes(pathSegments[0])) { 882 return pathSegments[0]; 883 } else { 884 return getCookie("sbox-l") || "en"; 885 } 886 } 887 888 function translateHeader(language) { 889 const resources = dataResource.i18nResources[language] || dataResource.i18nResources.en; 890 // Update text content - æé¤ç¨æ·èååå ¶åå ç´ 891 document.querySelectorAll('[data-i18n]').forEach(element => { 892 // æ£æ¥å ç´ æ¯å¦å¨ç¨æ·èåå 893 const isTargetElement = element.classList.contains('fp-dropdown-toggle') && 894 element.closest('.fp-user-menu-item'); 895 896 // 妿æ¯ç¨æ·èåå çå ç´ ï¼è·³è¿ç¿»è¯ 897 if (isTargetElement) { 898 return; 899 } 900
901 const keys = element.getAttribute('data-i18n').split('.'); 902 let value = resources; 903 904 for (const key of keys) { 905 value = value?.[key]; 906 if (value === undefined) break; 907 } 908 909 if (value !== undefined) { 910 element.textContent = value; 911 } 912 }); 913 914 // Update URLs - now using exact URLs from JSON without modification 915 updateAllUrls(language); 916 updateQuickShareButtonText(); 917 } 918 919 function updateAllUrls(language) { 920 document.querySelectorAll('[data-url-key]').forEach(element => { 921 const keys = element.getAttribute('data-url-key').split('.'); 922 923 // First try to get URL from current language resources 924 const currentLangItem = getItemByPath(dataResource.i18nResources[language], keys); 925 if (currentLangItem?.url) { 926 element.href = currentLangItem.url; 927 return; 928 } 929 930 // Fallback to English URL if current language doesn't have one 931 const enItem = getItemByPath(dataResource.i18nResources.en, keys); 932 if (enItem?.url) { 933 element.href = enItem.url; 934 } 935 }); 936 } 937 938 function getItemByPath(resource, pathKeys) { 939 let item = resource; 940 for (const key of pathKeys) { 941 item = item?.[key]; 942 if (item === undefined) return null; 943 } 944 return item; 945 } 946 947 function getQuickShareTexts() { 948 return dataResource?.i18nResources?.[currentLanguage]?.nav?.register?.quickShare || 949 dataResource?.i18nResources?.en?.nav?.register?.quickShare || 950 {}; 951 } 952 953 function hasPendingQuickShare() { 954 return sessionStorage.getItem(QUICK_SHARE_PENDING_KEY) === '1'; 955 } 956 957 function markPendingQuickShare() { 958 sessionStorage.setItem(QUICK_SHARE_PENDING_KEY, '1'); 959 } 960 961 function clearPendingQuickShare() { 962 sessionStorage.removeItem(QUICK_SHARE_PENDING_KEY); 963 } 964 965 function getPromoCodeFromUrl() { 966 try { 967 const urlParams = new URLSearchParams(window.location.search); 968 return urlParams.get('promo'); 969 } catch (e) { 970 console.error('Failed to parse promo from url:', e); 971 return null; 972 } 973 } 974 975 function savePromoCodeToStorage(code) { 976 if (!code) return; 977 try { 978 localStorage.setItem(PROMO_CODE_STORAGE_KEY, code); 979 } catch (e) { 980 console.error('Failed to save promo code:', e); 981 } 982 } 983 984 function getStoredPromoCode() { 985 try { 986 return localStorage.getItem(PROMO_CODE_STORAGE_KEY); 987 } catch (e) { 988 console.error('Failed to read promo code:', e); 989 return null; 990 } 991 } 992 993 function clearStoredPromoCode() { 994 try { 995 localStorage.removeItem(PROMO_CODE_STORAGE_KEY); 996 } catch (e) { 997 console.error('Failed to clear promo code:', e); 998 } 999 } 1000 1001 async function initPromoFromUrlAndValidate() { 1002 const promoCode = getPromoCodeFromUrl(); 1003 if (!promoCode) { 1004 return; 1005 } 1006 try { 1007 const resp = await fetch('/api/v1/coupons/validation?code=' + promoCode, { 1008 method: 'GET' 1009 }); 1010 let data = null; 1011 try { 1012 data = await resp.json(); 1013 } catch (e) { 1014 data = null; 1015 } 1016 if (!resp.ok || (data && data.code && data.code !== 100000)) { 1017 console.log('promo validation failed:', data || resp.status); 1018 reportLog('Promo_Validation_Failed', 'KOL_Promo', { 1019 source: promoCode, 1020 url: window.location.href, 1021 error: data?.message || data?.code || String(data) 1022 }) 1023 return; 1024 } 1025 savePromoCodeToStorage(promoCode); 1026 isPromoValidated = true; 1027 } catch (e) { 1028 console.log('promo validation error:', e); 1029 reportLog('Promo_Validation_Failed', 'KOL_Promo', { 1030 method: 'HTTP_ERROR', 1031 source: promoCode, 1032 url: window.location.href, 1033 error: e.message || String(e) 1034 }) 1035 } 1036 } 1037 1038 function getPromoTexts() { 1039 const resources = dataResource.i18nResources[currentLanguage] || dataResource.i18nResources.en; 1040 return resources.nav?.register || dataResource.i18nResources.en.nav.register; 1041 } 1042 1043 function openPromoLoginFlow() { 1044 // ä¼å 使ç¨å ¨ç«ç»å½å¼¹çª 1045 if (typeof window.fpOpenLoginModal === 'function') { 1046 try { 1047 window.fpOpenLoginModal(); 1048 return; 1049 } catch (e) { 1050 console.log('fpOpenLoginModal failed in promo dialog:', e); 1051 } 1052 } 1053 // åéå°åæè·³è½¬é»è¾ 1054 const returnUrl = `${location.pathname}${location.search}${location.hash}`; 1055 localStorage.setItem('register_from', returnUrl);
1056 setCookie('_login_return_url', encodeURIComponent(returnUrl)); 1057 window.location.href = '/auth/login'; 1058 } 1059 1060 function buildQuickShareButton() { 1061 if (document.querySelector('.fp-quick-share-btn')) { 1062 updateQuickShareButtonText(); 1063 return; 1064 } 1065 1066 const button = document.createElement('button'); 1067 button.type = 'button'; 1068 button.className = 'fp-quick-share-btn'; 1069 1070 const iconWrap = document.createElement('span'); 1071 iconWrap.className = 'fp-quick-share-btn-icon-wrap'; 1072 1073 const icon = document.createElement('img'); 1074 icon.className = 'fp-quick-share-btn-icon'; 1075 icon.src = `https://fronted.familypro.io/${window.location.href.includes('familypro') && !window.location.href.includes('dev.familypro') ? 'familypro' : 'familypro-dev'}/fronted/sections/images/promote.svg`; 1076 icon.alt = 'fp promote share icon'; 1077 iconWrap.appendChild(icon); 1078 1079 button.appendChild(iconWrap); 1080 document.body.appendChild(button); 1081 updateQuickShareButtonText(); 1082 } 1083 1084 function updateQuickShareButtonText() { 1085 const button = document.querySelector('.fp-quick-share-btn'); 1086 if (!button) { 1087 return; 1088 } 1089 const texts = getQuickShareTexts(); 1090 const tooltip = texts.tooltip || ''; 1091 button.title = tooltip; 1092 button.setAttribute('aria-label', tooltip); 1093 button.setAttribute('data-tooltip', tooltip); 1094 } 1095 1096 function setQuickShareButtonLoading(loading) { 1097 const button = document.querySelector('.fp-quick-share-btn'); 1098 if (!button) { 1099 return; 1100 } 1101 const texts = getQuickShareTexts(); 1102 const tooltip = loading ? (texts.loading || '') : (texts.tooltip || ''); 1103 button.disabled = loading; 1104 button.classList.toggle('is-loading', loading); 1105 button.title = tooltip; 1106 button.setAttribute('aria-label', tooltip); 1107 button.setAttribute('data-tooltip', tooltip); 1108 } 1109 1110 function buildQuickFeedbackButton() { 1111 if (document.querySelector('.fp-quick-feedback-btn')) { 1112 updateQuickFeedbackButtonText(); 1113 return; 1114 } 1115 1116 const button = document.createElement('button'); 1117 button.type = 'button'; 1118 button.className = 'fp-quick-feedback-btn'; 1119 1120 const iconWrap = document.createElement('span'); 1121 iconWrap.className = 'fp-quick-feedback-btn-icon-wrap'; 1122 1123 const icon = document.createElement('img'); 1124 icon.className = 'fp-quick-feedback-btn-icon'; 1125 icon.src = location.href.indexOf('localhost') == -1 ? 1126 `https://fronted.familypro.io/${window.location.href.includes('familypro') && !window.location.href.includes('dev.familypro') ? 'familypro' : 'familypro-dev'}/fronted/sections/images/feedback.svg` : 1127 `/fronted/sections/images/feedback.svg`; 1128 icon.alt = 'fp feedback icon'; 1129 iconWrap.appendChild(icon); 1130 1131 button.appendChild(iconWrap); 1132 document.body.appendChild(button); 1133 updateQuickFeedbackButtonText(); 1134 } 1135 1136 function updateQuickFeedbackButtonText() { 1137 const button = document.querySelector('.fp-quick-feedback-btn'); 1138 if (!button) { 1139 return; 1140 } 1141 const texts = getQuickFeedbackTexts(); 1142 const tooltip = texts.tooltip || ''; 1143 button.title = tooltip; 1144 button.setAttribute('aria-label', tooltip); 1145 button.setAttribute('data-tooltip', tooltip); 1146 } 1147 1148 function handleQuickFeedbackClick(event) { 1149 event.preventDefault(); 1150 if (typeof window.fpOpenFeedbackCreateModal === 'function') { 1151 try { 1152 reportLog('Quick_Feedback', 'Feedback', { 1153 method: 'open_modal', 1154 source: 'FamilyPro' 1155 }); 1156 window.fpOpenFeedbackCreateModal({}); 1157 return; 1158 } catch (err) { 1159 console.warn('fpOpenFeedbackCreateModal failed:', err); 1160 } 1161 } 1162 reportLog('Quick_Feedback', 'Feedback', { 1163 method: 'open_page', 1164 source: 'FamilyPro' 1165 }); 1166 var url = '/feedback'; 1167 window.open(url, '_blank', 'noopener,noreferrer'); 1168 } 1169 1170 function getQuickFeedbackTexts() { 1171 return dataResource?.i18nResources?.[currentLanguage]?.nav?.register?.quickFeedback || 1172 dataResource?.i18nResources?.en?.nav?.register?.quickFeedback || 1173 {}; 1174 } 1175 1176 function openQuickShareLoginFlow() { 1177 const texts = getQuickShareTexts(); 1178 markPendingQuickShare(); 1179 showToast(texts.loginRequired, 'error'); 1180 if (typeof window.fpOpenLoginModal === 'function') { 1181 try { 1182 window.fpOpenLoginModal(); 1183 return; 1184 } catch (error) { 1185 console.log('fpOpenLoginModal failed:', error); 1186 } 1187 } 1188 const returnUrl = `${location.pathname}${location.search}${location.hash}`; 1189 localStorage.setItem('register_from', returnUrl);
1190 setCookie('_login_return_url', encodeURIComponent(returnUrl)); 1191 window.location.href = '/auth/login'; 1192 } 1193 1194 async function ensureQuickShareInvitationLink(forceRefresh) { 1195 if (!forceRefresh && quickShareCache?.invitationLink) { 1196 return quickShareCache.invitationLink; 1197 } 1198 1199 const token = getLocalTokenStr(); 1200 if (!token) { 1201 throw new Error('AUTH_REQUIRED'); 1202 } 1203 1204 const response = await fetch(CONFIG.userCenter, { 1205 headers: { 1206 'Authorization': token 1207 } 1208 }); 1209 1210 if (!response) { 1211 throw new Error('AUTH_REQUIRED'); 1212 return null; 1213 } 1214 1215 if (response.status === 401 || response.status === 403) { 1216 quickShareCache = null; 1217 isAuthenticated = false; 1218 openQuickShareLoginFlow(); 1219 return null; 1220 } 1221 1222 if (!response.ok) { 1223 throw new Error(`HTTP error! status: ${response.status}`); 1224 } 1225 1226 const result = await response.json(); 1227 if (result?.code !== 100000 || !result?.data?.invitation_code) { 1228 throw new Error(result?.message || 'INVALID_INVITATION_LINK'); 1229 } 1230 1231 quickShareCache = { 1232 invitationLink: result.data.invitation_code 1233 }; 1234 1235 return quickShareCache.invitationLink; 1236 } 1237 1238 function extractInviteCode(rawInvitationLink) { 1239 if (!rawInvitationLink) { 1240 return ''; 1241 } 1242 1243 try { 1244 const parsedUrl = new URL(rawInvitationLink, window.location.origin); 1245 const inviteFromQuery = parsedUrl.searchParams.get('invite'); 1246 if (inviteFromQuery) { 1247 return inviteFromQuery; 1248 } 1249 1250 const pathSegments = parsedUrl.pathname.split('/').filter(Boolean); 1251 const lastSegment = pathSegments[pathSegments.length - 1]; 1252 return lastSegment || rawInvitationLink; 1253 } catch (error) { 1254 return rawInvitationLink; 1255 } 1256 } 1257 1258 function buildShareLink(rawInvitationLink) { 1259 const inviteCode = extractInviteCode(rawInvitationLink); 1260 const targetUrl = new URL(window.location.origin + window.location.pathname); 1261 if (inviteCode) { 1262 targetUrl.searchParams.set('invite', inviteCode); 1263 } 1264 return targetUrl.toString(); 1265 } 1266 1267 function buildQuickSharePayload(invitationLink) { 1268 const texts = getQuickShareTexts(); 1269 const shareLink = buildShareLink(invitationLink); 1270 return { 1271 invitationLink: shareLink, 1272 inviteCode: extractInviteCode(invitationLink), 1273 shareTitle: texts.title, 1274 shareText: (texts.shareMessage || '{link}').replace('{link}', shareLink) 1275 }; 1276 } 1277 1278 async function copyQuickShareLink(invitationLink) { 1279 const texts = getQuickShareTexts(); 1280 if (navigator.clipboard && navigator.clipboard.writeText) { 1281 try { 1282 await navigator.clipboard.writeText(invitationLink); 1283 showToast(texts.copied, 'success'); 1284 return; 1285 } catch (error) { 1286 console.log('Copy with clipboard failed:', error); 1287 } 1288 } 1289 1290 const input = document.createElement('input'); 1291 input.value = invitationLink; 1292 document.body.appendChild(input); 1293 input.select(); 1294 try { 1295 const success = document.execCommand('copy'); 1296 if (success) { 1297 showToast(texts.copied, 'success'); 1298 } else { 1299 showToast(texts.copyFailed, 'error'); 1300 } 1301 } catch (error) { 1302 console.log('Fallback copy failed:', error); 1303 showToast(texts.copyFailed, 'error'); 1304 } finally { 1305 document.body.removeChild(input); 1306 } 1307 } 1308 1309 function closeQuickShareModal() { 1310 if (!quickShareOverlay) { 1311 return; 1312 } 1313 const activeOverlay = quickShareOverlay; 1314 quickShareOverlay = null; 1315 document.body.classList.remove('fp-share-modal-open'); 1316 document.removeEventListener('keydown', handleQuickShareKeydown); 1317 activeOverlay.classList.remove('fp-show'); 1318 window.setTimeout(() => { 1319 if (activeOverlay.parentNode) { 1320 activeOverlay.parentNode.removeChild(activeOverlay); 1321 } 1322 }, 180); 1323 } 1324 1325 function handleQuickShareKeydown(event) { 1326 if (event.key === 'Escape') { 1327 closeQuickShareModal(); 1328 } 1329 } 1330 1331 function createQuickShareAction(item) { 1332 const button = document.createElement('button'); 1333 button.type = 'button'; 1334 button.className = `fp-share-action fp-share-action-${item.key}`; 1335 button.title = item.label; 1336 button.setAttribute('aria-label', item.label); 1337 button.style.backgroundColor = item.background; 1338 button.innerHTML = item.icon; 1339 button.addEventListener('click', () => { 1340 reportLog('Social_Share', 'Quick_Share', { 1341 method: item.key, 1342 url: item.invitationLink 1343 }); 1344 if (item.key === 'email') { 1345 window.location.href = item.url; 1346 } else { 1347 window.open(item.url, '_blank', 'noopener,noreferrer,width=760,height=640'); 1348 } 1349 }); 1350 1351 return button; 1352 } 1353 1354 const COPY_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>' 1355 const X_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2H21l-6.58 7.52L22 22h-5.956l-4.663-6.103L6.04 22H3.28l7.037-8.043L2 2h6.108l4.215 5.574zM17.278 20.2h1.526L7.279 3.705H5.64z"/></svg>' 1356 const TELEGRAM_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M9.036 15.773 8.66 21.06c.538 0 .77-.23 1.048-.507l2.516-2.406 5.214 3.816c.956.528 1.63.25 1.888-.88l3.42-16.02.001-.001c.303-1.414-.511-1.968-1.441-1.622L1.434 11.1C.063 11.628.083 12.39 1.2
135612.732l5.084 1.586L18.09 6.96c.556-.368 1.062-.164.646.204"/></svg>' 1357 const FACEBOOK_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M13.5 22v-8h2.7l.4-3h-3.1V9.1c0-.9.3-1.5 1.6-1.5h1.7V4.9c-.3 0-1.3-.1-2.4-.1-2.4 0-4 1.5-4 4.2V11H8v3h2.4v8z"/></svg>' 1358 const REDDIT_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M14.2 15.6c-.5.5-1.4.8-2.2.8-.8 0-1.7-.3-2.2-.8-.2-.2-.2-.5 0-.7.2-.2.5-.2.7 0 .3.3.9.5 1.5.5s1.2-.2 1.5-.5c.2-.2.5-.2.7 0 .2.2.2.5 0 .7M9.4 13.1c-.6 0-1-.5-1-1 0-.6.5-1 1-1 .6 0 1 .5 1 1s-.5 1-1 1m5.2 0c-.6 0-1-.5-1-1 0-.6.5-1 1-1 .6 0 1 .5 1 1s-.4 1-1 1m7.4-1.1c0-1.5-1.2-2.7-2.7-2.7-.7 0-1.3.2-1.8.7-1.4-.9-3.3-1.4-5.3-1.5l1.1-3.4 2.9.7c0 1 .8 1.8 1.8 1.8S20 7 20 6s-.8-1.8-1.8-1.8c-.7 0-1.4.4-1.6 1L13 4.3c-.3-.1-.5.1-.6.3l-1.3 4C9.1 8.6 7.2 9.1 5.8 10c-.5-.4-1.1-.7-1.8-.7-1.5 0-2.7 1.2-2.7 2.7 0 1 .6 1.9 1.4 2.4 0 .2 0 .4 0 .6 0 3.1 3.8 5.6 8.5 5.6s8.5-2.5 8.5-5.6c0-.2 0-.4 0-.6.9-.5 1.5-1.4 1.5-2.4"/></svg>' 1359 const EMAIL_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="m3 7 9 6 9-6"></path></svg>' 1360 1361 function createQuickShareModal(payload) { 1362 const texts = getQuickShareTexts(); 1363 const encodedLink = encodeURIComponent(payload.invitationLink); 1364 const encodedText = encodeURIComponent(payload.shareText); 1365 const encodedTitle = encodeURIComponent(payload.shareTitle); 1366 const shareActions = [ 1367 { 1368 key: 'twitter', 1369 label: 'Twitter', 1370 icon: X_ICON, 1371 background: '#111827', 1372 url: `https://twitter.com/intent/tweet?url=${encodedLink}&text=${encodedText}`, 1373 invitationLink: payload.invitationLink 1374 }, 1375 { 1376 key: 'telegram', 1377 label: 'Telegram', 1378 icon: TELEGRAM_ICON, 1379 background: '#4AA9E9', 1380 url: `https://t.me/share/url?url=${encodedLink}&text=${encodedText}`, 1381 invitationLink: payload.invitationLink 1382 }, 1383 { 1384 key: 'facebook', 1385 label: 'Facebook', 1386 icon: FACEBOOK_ICON, 1387 background: '#3B82F6', 1388 url: `https://www.facebook.com/sharer/sharer.php?u=${encodedLink}`, 1389 invitationLink: payload.invitationLink 1390 }, 1391 { 1392 key: 'reddit', 1393 label: 'Reddit', 1394 icon: REDDIT_ICON, 1395 background: '#FF7A33', 1396 url: `https://www.reddit.com/submit?url=${encodedLink}&title=${encodedTitle}`, 1397 invitationLink: payload.invitationLink 1398 }, 1399 { 1400 key: 'email', 1401 label: 'Email', 1402 icon: EMAIL_ICON, 1403 background: '#94A3B8', 1404 url: `mailto:?subject=${encodedTitle}&body=${encodedText}`, 1405 invitationLink: payload.invitationLink 1406 } 1407 ]; 1408 1409 const overlay = document.createElement('div'); 1410 overlay.className = 'fp-share-modal-overlay'; 1411 overlay.addEventListener('click', (event) => { 1412 if (event.target === overlay) { 1413 closeQuickShareModal(); 1414 } 1415 }); 1416 1417 const modal = document.createElement('div'); 1418 modal.className = 'fp-share-modal'; 1419 1420 const closeBtn = document.createElement('button'); 1421 closeBtn.type = 'button'; 1422 closeBtn.className = 'fp-share-modal-close'; 1423 closeBtn.setAttribute('aria-label', 'Close'); 1424 closeBtn.innerHTML = '×'; 1425 closeBtn.addEventListener('click', closeQuickShareModal); 1426 1427 const title = document.createElement('h3'); 1428 title.className = 'fp-share-modal-title'; 1429 title.textContent = texts.title; 1430 1431 const subtitle = document.createElement('p'); 1432 subtitle.className = 'fp-share-modal-subtitle'; 1433 subtitle.textContent = texts.subtitle; 1434 1435 const linkGroup = document.createElement('div'); 1436 linkGroup.className = 'fp-share-link-group'; 1437 const linkInput = document.createElement('input'); 1438 linkInput.className = 'fp-share-link-input'; 1439 linkInput.type = 'text'; 1440 linkInput.readOnly = true; 1441 linkInput.value = payload.invitationLink; 1442 1443 const copyBtn = document.createElement('button'); 1444 copyBtn.type = 'button'; 1445 copyBtn.className = 'fp-share-copy-btn'; 1446 copyBtn.title = texts.copy || ''; 1447 copyBtn.setAttribute('aria-label', texts.copy || ''); 1448 copyBtn.innerHTML = COPY_ICON; 1449 copyBtn.addEventListener('click', () => { 1450 reportLog('Copy_Link', 'Quick_Share', { 1451 url: payload.invitationLink 1452 }); 1453 copyQuickShareLink(payload.invitationLink); 1454 }); 1455 1456 linkInput.setAttribute('aria-label', texts.invitationLabel || texts.title || ''); 1457 linkGroup.appendChild(linkInput); 1458 linkGroup.appendChild(copyBtn); 1459 1460 const platformGrid = document.createElement('div'); 1461 platformGrid.className = 'fp-share-actions-grid'; 1462 shareActions.forEach((item) => { 1463 platformGrid.appendChild(createQuickShareAction(item)); 1464 }); 1465 1466 modal.appendChild(closeBtn); 1467 modal.appendChild(title); 1468 modal.appendChild(subtitle); 1469 modal.appendChild(linkGroup); 1470 modal.appendChild(platformGrid); 1471 overlay.appendChild(modal); 1472 1473 return overlay; 1474 } 1475 1476 async function openQuickShareModal(options = {}) {
1477 try { 1478 setQuickShareButtonLoading(true); 1479 const invitationLink = await ensureQuickShareInvitationLink(options.forceRefresh); 1480 if (!invitationLink) { 1481 return; 1482 } 1483 const payload = buildQuickSharePayload(invitationLink); 1484 closeQuickShareModal(); 1485 quickShareOverlay = createQuickShareModal(payload); 1486 document.body.appendChild(quickShareOverlay); 1487 document.body.classList.add('fp-share-modal-open'); 1488 document.addEventListener('keydown', handleQuickShareKeydown); 1489 requestAnimationFrame(() => { 1490 if (quickShareOverlay) { 1491 quickShareOverlay.classList.add('fp-show'); 1492 } 1493 }); 1494 } catch (error) { 1495 if (error.message === 'AUTH_REQUIRED' || error.message.includes('401')) { 1496 quickShareCache = null; 1497 isAuthenticated = false; 1498 openQuickShareLoginFlow(); 1499 } else { 1500 console.error('Failed to open quick share modal:', error); 1501 showToast(getQuickShareTexts().shareLoadFailed, 'error'); 1502 } 1503 } finally { 1504 setQuickShareButtonLoading(false); 1505 } 1506 } 1507 1508 async function maybeOpenPendingQuickShare() { 1509 if (!isAuthenticated || !hasPendingQuickShare()) { 1510 return; 1511 } 1512 clearPendingQuickShare(); 1513 await openQuickShareModal(); 1514 } 1515 1516 async function handleQuickShareClick(event) { 1517 event.preventDefault(); 1518 if (document.querySelector('.fp-quick-share-btn')?.classList.contains('is-loading')) { 1519 return; 1520 } 1521 reportLog('Quick_Share_Click', 'Quick_Share', { 1522 url: window.location.pathname 1523 }); 1524 const token = getLocalTokenStr(); 1525 if (!token && !isAuthenticated) { 1526 openQuickShareLoginFlow(); 1527 return; 1528 } 1529 await openQuickShareModal(); 1530 } 1531 1532 1533 1534 // ==================== Auth Functions ==================== 1535 function readStoredAuth() { 1536 try { 1537 const tokenData = JSON.parse(localStorage.getItem('token') || 'null'); 1538 const userInfo = JSON.parse(localStorage.getItem('userInfo') || 'null'); 1539 return { tokenData, userInfo }; 1540 } catch (error) { 1541 console.log('Failed to read stored auth state:', error); 1542 return { tokenData: null, userInfo: null }; 1543 } 1544 } 1545 1546 function isStoredTokenExpired(tokenData) { 1547 if (!tokenData?.token) return true; 1548 try { 1549 const payloadSegment = tokenData.token.split('.')[1]; 1550 const base64 = payloadSegment.replace(/-/g, '+').replace(/_/g, '/'); 1551 const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '='); 1552 const payload = JSON.parse(atob(padded)); 1553 if (payload.exp) { 1554 return Math.floor(Date.now() / 1000) >= Number(payload.exp); 1555 } 1556 } catch (_error) { 1557 // Fall back to the stored expiration time for legacy sessions. 1558 } 1559 1560 const storedExpiration = Number(tokenData.expirationTime); 1561 if (!Number.isFinite(storedExpiration) || storedExpiration <= 0) return true; 1562 const expirationMs = storedExpiration < 1e12 ? storedExpiration * 1000 : storedExpiration; 1563 return Date.now() >= expirationMs; 1564 } 1565 1566 async function refreshStoredAuth(userInfo) { 1567 try { 1568 if (typeof window.fpRefreshAccessToken === 'function') { 1569 await window.fpRefreshAccessToken(); 1570 return readStoredAuth(); 1571 } 1572 1573 const previousToken = readStoredAuth().tokenData?.token; 1574 const refreshOnce = async () => { 1575 const currentAuth = readStoredAuth(); 1576 if ( 1577 currentAuth.tokenData?.token && 1578 currentAuth.tokenData.token !== previousToken && 1579 !isStoredTokenExpired(currentAuth.tokenData) 1580 ) { 1581 return currentAuth; 1582 } 1583 1584 const response = await fetch(CONFIG.refreshUrl, { 1585 method: 'POST', 1586 credentials: 'include', 1587 headers: { 'Content-Type': 'application/json' } 1588 }); 1589 if (!response?.ok) return null; 1590 1591 const result = await response.json(); 1592 const data = result?.data; 1593 if (!data?.access_token) return null; 1594 1595 const nextUser = data.user || userInfo; 1596 const tokenData = { 1597 token: data.access_token, 1598 expirationTime: Date.now() + Number(data.expires_in || 0) * 1000 1599 }; 1600 localStorage.setItem('token', JSON.stringify(tokenData)); 1601 if (nextUser) localStorage.setItem('userInfo', JSON.stringify(nextUser)); 1602 return { tokenData, userInfo: nextUser }; 1603 }; 1604 1605 if (navigator.locks?.request) { 1606 return navigator.locks.request('familypro-auth-refresh', refreshOnce); 1607 } 1608 return refreshOnce(); 1609 } catch (error) { 1610 console.log('Failed to refresh auth state:', error); 1611 return null; 1612 } 1613 } 1614 1615 async function checkAuthState() {
1616 try { 1617 const popupShown = sessionStorage.getItem('popupShown'); 1618 let { tokenData, userInfo } = readStoredAuth(); 1619 if (userInfo && tokenData?.token && isStoredTokenExpired(tokenData)) { 1620 const refreshed = await refreshStoredAuth(userInfo); 1621 tokenData = refreshed?.tokenData || null; 1622 userInfo = refreshed?.userInfo || null; 1623 } 1624 const loginFlag = Boolean(userInfo && tokenData?.token && !isStoredTokenExpired(tokenData)); 1625 1626 if (!loginFlag) { 1627 const inviteCode = getInviteCode(); 1628 const startOfToday = new Date(new Date().setHours(0, 0, 0, 0)).getTime(); 1629 1630 if (inviteCode != null) { 1631 if (!popupShown || (popupShown && popupShown != startOfToday)) { 1632 const resources = dataResource.i18nResources[currentLanguage] || dataResource.i18nResources.en; 1633 showWelcomeDialog(resources.nav.register.couponRegisterTitle, 80, resources.nav.register.couponRegisterBtn, '/auth/register'); 1634 } 1635 } else { 1636 if (!popupShown || (popupShown && popupShown != startOfToday)) { 1637 const resources = dataResource.i18nResources[currentLanguage] || dataResource.i18nResources.en; 1638 showWelcomeDialog(resources.nav.register.couponRegisterTitle, 40, resources.nav.register.couponRegisterBtn, '/auth/register') 1639 } 1640 } 1641 } 1642 1643 1644 isAuthenticated = loginFlag; 1645 1646 document.querySelectorAll('.fp-nav-item').forEach(item => { 1647 const authRequired = item.getAttribute('data-auth') === 'true'; 1648 const authForbidden = item.getAttribute('data-auth') === 'false'; 1649 1650 if (authRequired) { 1651 item.style.display = loginFlag ? '' : 'none'; 1652 } else if (authForbidden) { 1653 item.style.display = loginFlag ? 'none' : ''; 1654 } 1655 }); 1656 1657 // æ´æ°ç¨æ·èåçé®ç®±æ¾ç¤º 1658 if (loginFlag && userInfo) { 1659 const user = userInfo; 1660 document.querySelectorAll('.fp-user-menu-item .fp-user-email').forEach(el => { 1661 if (user.email) { 1662 el.textContent = user.email; 1663 window.$crisp.push(['set', 'user:email', [user.email]]); 1664 } 1665 }); 1666 } 1667 1668 return loginFlag; 1669 } catch (error) { 1670 isAuthenticated = false; 1671 console.error('Auth check failed:', error); 1672 return false; 1673 } 1674 } 1675 1676 // ==================== Toast Notification ==================== 1677 /** 1678 * @param {string} message 1679 * @param {'error'|'success'} [type='error'] 1680 * @param {number} [durationSeconds=3] èªå¨å ³éç§æ°ï¼ä¼ 0 表示ä¸èªå¨å ³é 1681 */ 1682 function showToast(message, type = 'error', durationSeconds = 3) { 1683 // å建 toast 容å¨ï¼å¦æä¸åå¨ï¼ 1684 let toastContainer = document.getElementById('fp-toast-container'); 1685 if (!toastContainer) { 1686 toastContainer = document.createElement('div'); 1687 toastContainer.id = 'fp-toast-container'; 1688 toastContainer.style.cssText = ` 1689 position: fixed; 1690 top: 20px; 1691 left: 50%; 1692 transform: translateX(-50%); 1693 z-index: 10000; 1694 display: flex; 1695 flex-direction: column; 1696 gap: 10px; 1697 pointer-events: none; 1698 `; 1699 document.body.appendChild(toastContainer); 1700 } 1701 1702 // å建 toast å ç´ 1703 const toast = document.createElement('div'); 1704 toast.style.cssText = ` 1705 background: #fff; 1706 border-radius: 8px; 1707 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 1708 padding: 16px 20px; 1709 min-width: 300px; 1710 max-width: 500px; 1711 display: flex; 1712 align-items: center; 1713 gap: 12px; 1714 pointer-events: auto; 1715 animation: slideInTop 0.3s ease-out; 1716 border-left: 4px solid ${type === 'error' ? '#f56565' : '#48bb78'}; 1717 `; 1718 1719 // æ·»å å¨ç»æ ·å¼ï¼å¦æä¸åå¨ï¼ 1720 if (!document.getElementById('fp-toast-styles')) { 1721 const style = document.createElement('style'); 1722 style.id = 'fp-toast-styles'; 1723 style.textContent = ` 1724 @keyframes slideInTop { 1725 from { 1726 transform: translateY(-100%); 1727 opacity: 0; 1728 } 1729 to { 1730 transform: translateY(0); 1731 opacity: 1; 1732 } 1733 } 1734 @keyframes slideOutTop { 1735 from { 1736 transform: translateY(0); 1737 opacity: 1; 1738 } 1739 to { 1740 transform: translateY(-100%); 1741 opacity: 0; 1742 } 1743 } 1744 `; 1745 document.head.appendChild(style); 1746 } 1747 1748 // åå»ºå¾æ 1749 const icon = document.createElement('div'); 1750 icon.style.cssText = `
1751 width: 24px; 1752 height: 24px; 1753 border-radius: 50%; 1754 background: ${type === 'error' ? '#f56565' : '#48bb78'}; 1755 display: flex; 1756 align-items: center; 1757 justify-content: center; 1758 flex-shrink: 0; 1759 color: white; 1760 font-size: 14px; 1761 font-weight: bold; 1762 `; 1763 icon.textContent = type === 'error' ? '!' : 'â'; 1764 1765 // åå»ºæ¶æ¯ææ¬ 1766 const messageText = document.createElement('div'); 1767 messageText.style.cssText = ` 1768 flex: 1; 1769 color: #2d3748; 1770 font-size: 14px; 1771 line-height: 1.5; 1772 word-wrap: break-word; 1773 white-space: pre-line; 1774 `; 1775 messageText.innerHTML = message.replace(/\n/g, '<br>'); 1776 1777 // åå»ºå ³éæé® 1778 const closeBtn = document.createElement('button'); 1779 closeBtn.style.cssText = ` 1780 background: none; 1781 border: none; 1782 color: #718096; 1783 cursor: pointer; 1784 padding: 4px; 1785 font-size: 18px; 1786 line-height: 1; 1787 opacity: 0.6; 1788 transition: opacity 0.2s; 1789 `; 1790 closeBtn.innerHTML = 'Ã'; 1791 closeBtn.onmouseover = () => closeBtn.style.opacity = '1'; 1792 closeBtn.onmouseout = () => closeBtn.style.opacity = '0.6'; 1793 closeBtn.onclick = () => removeToast(toast); 1794 1795 // ç»è£ toast 1796 toast.appendChild(icon); 1797 toast.appendChild(messageText); 1798 toast.appendChild(closeBtn); 1799 toastContainer.appendChild(toast); 1800 1801 const autoCloseSec = Number(durationSeconds); 1802 if (Number.isFinite(autoCloseSec) && autoCloseSec > 0) { 1803 toast._fpAutoCloseTimer = setTimeout(() => removeToast(toast), autoCloseSec * 1000); 1804 } 1805 } 1806 1807 function removeToast(toast) { 1808 if (toast._fpAutoCloseTimer) { 1809 clearTimeout(toast._fpAutoCloseTimer); 1810 toast._fpAutoCloseTimer = null; 1811 } 1812 if (toast._fpRemoving) return; 1813 toast._fpRemoving = true; 1814 toast.style.animation = 'slideOutTop 0.3s ease-out'; 1815 setTimeout(() => { 1816 if (toast.parentNode) { 1817 toast.parentNode.removeChild(toast); 1818 } 1819 // 妿容å¨ä¸ºç©ºï¼ç§»é¤å®¹å¨ 1820 const container = document.getElementById('fp-toast-container'); 1821 if (container && container.children.length === 0) { 1822 container.remove(); 1823 } 1824 }, 300); 1825 } 1826 1827 // ==================== Logout Function ==================== 1828 async function performLogout() { 1829 localStorage.removeItem('token'); 1830 localStorage.removeItem('userInfo'); 1831 isAuthenticated = false; 1832 quickShareCache = null; 1833 closeQuickShareModal(); 1834 try { 1835 await fetch(CONFIG.logoutUrl, { 1836 method: 'POST', 1837 credentials: 'include' 1838 }); 1839 } catch (error) { 1840 console.log('Logout request failed:', error); 1841 } 1842 crispReset(); 1843 } 1844 1845 1846 function getInviteCode() { 1847 // ä¼å ä»urlä¸è·åææ°é请ç ï¼urlæ²¡ææ¶åå°è¯ä»æ¬å°è·å 1848 const urlParams = new URLSearchParams(window.location.search); 1849 let inviteCode = urlParams.get('invite'); 1850 if (inviteCode !== null) { 1851 localStorage.setItem("inviteCode", inviteCode) 1852 return inviteCode 1853 } 1854 inviteCode = localStorage.getItem('inviteCode') 1855 return inviteCode; 1856 } 1857 1858 // ==================== Utility Functions ==================== 1859 function getCookie(name) { 1860 const cookies = document.cookie.split(';'); 1861 for (let cookie of cookies) { 1862 const [cookieName, cookieValue] = cookie.trim().split('='); 1863 if (cookieName === name) { 1864 return cookieValue; 1865 } 1866 } 1867 return null; 1868 } 1869 1870 function setCookie(name, value) { 1871 document.cookie = `${name}=${value};path=/;SameSite=Lax`; 1872 } 1873 1874 function getLocalTokenStr() { 1875 try { 1876 let tokenStr = JSON.parse(localStorage.getItem("token")).token; 1877 return tokenStr; 1878 } catch (err) { 1879 console.log("Can not get token from localStorage: ", err); 1880 return null; 1881 } 1882 } 1883 1884 async function redeemPromoIfNeeded() { 1885 const promoCode = getStoredPromoCode(); 1886 if (!promoCode) { 1887 return; 1888 } 1889 1890 const token = getLocalTokenStr(); 1891 if (!token || !isAuthenticated) { 1892 return; 1893 } 1894
1895 try { 1896 const resp = await fetch('/api/v1/coupon-redemptions', { 1897 method: 'POST', 1898 headers: { 1899 'Content-Type': 'application/json', 1900 'Authorization': token 1901 }, 1902 body: JSON.stringify({ code: promoCode }) 1903 }); 1904 1905 let data = null; 1906 try { 1907 data = await resp.json(); 1908 } catch (e) { 1909 data = null; 1910 } 1911 1912 if (!resp.ok || (data && data.code && data.code !== 100000)) { 1913 console.error('Promo redemption failed:', data || resp.status); 1914 clearStoredPromoCode(); 1915 reportLog('Promo_Redemption', 'KOL_Promo', { 1916 status: 0, 1917 source: promoCode, 1918 url: window.location.href, 1919 error: data?.message || data?.code || String(data) 1920 }) 1921 return; 1922 } 1923 1924 clearStoredPromoCode(); 1925 const resources = dataResource.i18nResources[currentLanguage] || dataResource.i18nResources.en; 1926 const message = resources.nav?.register?.promoSuccess || 1927 dataResource.i18nResources.en.nav.register.promoSuccess; 1928 if (message) { 1929 showToast(message, 'success'); 1930 } 1931 reportLog('Promo_Redemption', 'KOL_Promo', { 1932 status: 200, 1933 source: promoCode, 1934 url: window.location.href, 1935 }) 1936 } catch (e) { 1937 console.error('Failed to redeem promo code:', e); 1938 reportLog('Promo_Redemption', 'KOL_Promo', { 1939 status: 0, 1940 method: 'HTTP_ERROR', 1941 source: promoCode, 1942 url: window.location.href, 1943 error: e?.message || String(e) 1944 }) 1945 } 1946 } 1947 1948 // ==================== Event Handlers ==================== 1949 function setupEventListeners() { 1950 document.querySelector('.fp-language-item .fp-dropdown-menu').addEventListener('click', handleLanguageChange) 1951 document.querySelector('.fp-mobile-menu-btn')?.addEventListener('click', toggleMobileMenu); 1952 document.querySelector('.fp-quick-share-btn')?.addEventListener('click', handleQuickShareClick); 1953 document.querySelector('.fp-quick-feedback-btn')?.addEventListener('click', handleQuickFeedbackClick); 1954 setupDropdownMenus(); 1955 document.getElementById('fp-logoutBtn')?.addEventListener('click', handleLogout); 1956 // login 1957 document.querySelector('a[data-url-key="nav.login"]').addEventListener('click', function (event) { 1958 event.preventDefault(); 1959 // ä¼å å°è¯è°ç¨ Vue åºç¨æè½½çå ¨å±å½æ° 1960 if (typeof window.fpOpenLoginModal === 'function') { 1961 try { 1962 window.fpOpenLoginModal(); 1963 toggleMobileMenu(); 1964 return; 1965 } catch (e) { 1966 console.log("fpOpenLoginModal failed:", e); 1967 } 1968 } 1969 // åéå°åæè·³è½¬é»è¾ 1970 const returnUrl = `${location.pathname}${location.search}${location.hash}`; 1971 localStorage.setItem('register_from', returnUrl); 1972 setCookie('_login_return_url', encodeURIComponent(returnUrl)); 1973 window.location.href = '/auth/login'; 1974 return; 1975 }); 1976 1977 // register 1978 document.querySelector('a[data-url-key="nav.register"]').addEventListener('click', function (event) { 1979 event.preventDefault(); 1980 1981 // åéå°åæè·³è½¬é»è¾ 1982 const returnUrl = `${location.pathname}${location.search}${location.hash}`; 1983 localStorage.setItem('register_from', returnUrl); 1984 setCookie('_login_return_url', encodeURIComponent(returnUrl)); 1985 window.location.href = '/auth/register'; 1986 return; 1987 }); 1988 1989 // Listen for user login success event 1990 window.addEventListener('user-login-success', function(event) { 1991 // Update header menu and auth state 1992 checkAuthState().then(async () => { 1993 if (isAuthenticated) { 1994 await redeemPromoIfNeeded(); 1995 if (hasPendingQuickShare()) { 1996 clearPendingQuickShare(); 1997 openQuickShareModal(); 1998 } 1999 } 2000 }).catch((error) => { 2001 console.error('Failed to update header after login:', error); 2002 }); 2003 }); 2004 } 2005 2006 function handleLanguageChange(e) { 2007 e.preventDefault(); 2008 e.stopPropagation(); 2009 if (e.target.classList.contains('fp-dropdown-item')) { 2010 // let targetKey = e.target.data-url-key 2011 const targetKey = e.target.getAttribute('data-url-key'); 2012 console.log("data-url-key", targetKey); 2013 // if(targetKey) 2014 let newLanguage = null; 2015 if (targetKey.includes("nav.language")) { 2016 let dataUrl = e.target.getAttribute('url-data') 2017 newLanguage = dataUrl.substring(dataUrl.lastIndexOf('/') + 1); 2018 } else { 2019 newLanguage = e.target.href.substring(e.target.href.lastIndexOf('/') + 1); 2020 } 2021 currentLanguage = newLanguage; 2022 localStorage.setItem('language', newLanguage);
2023 setCookie(CONFIG.languageCookieName, newLanguage); 2024 2025 const queryAndHash = `${window.location.search}${window.location.hash}`; 2026 2027 if(window.location.href.includes("blog")) { 2028 window.location.href = `/${newLanguage}/blog${queryAndHash}`; 2029 return; 2030 } 2031 2032 2033 2034 2035 translateHeader(newLanguage); 2036 2037 const newUrl = 2038 replaceLanguageInPath(window.location.pathname, currentLanguage) + queryAndHash; 2039 window.location.href = newUrl; 2040 return; 2041 } 2042 2043 } 2044 2045 function replaceLanguageInPath(path, newLang) { 2046 newLang = newLang.replace(/^\/|\/$/g, ''); 2047 2048 // æ£æ¥è·¯å¾æ¯å¦ä»¥è¯è¨ä»£ç å¼å¤´ï¼å¦ "/en" æ "/en/..."ï¼ 2049 const foundLang = languageList.find(lang => { 2050 const langPrefix = `/${lang}`; 2051 return path.startsWith(langPrefix + '/') || path === langPrefix; 2052 }); 2053 2054 if (foundLang) { 2055 // æ¿æ¢å¼å¤´çè¯è¨é¨åï¼å¦ "/en/order" â "/fr/order"ï¼ 2056 return path.replace(`/${foundLang}`, `/${newLang}`); 2057 } 2058 if (path === '/') { 2059 return `/${newLang}`; 2060 } 2061 return path; // æªæ¾å°è¯è¨ç¼©åï¼è¿ååè·¯å¾ 2062 } 2063 2064 function toggleMobileMenu() { 2065 const navMenu = document.querySelector('.fp-nav-menu'); 2066 const mobileBtn = document.querySelector('.fp-mobile-menu-btn'); 2067 2068 navMenu.classList.toggle('fp-active'); 2069 mobileBtn.classList.toggle('fp-active'); 2070 document.body.classList.toggle('fp-mobile-menu-open'); 2071 } 2072 2073 function getDropdownMenuForToggle(toggle) { 2074 const menu = toggle?.nextElementSibling; 2075 return menu?.classList?.contains('fp-dropdown-menu') ? menu : null; 2076 } 2077 2078 function setDropdownToggleActive(toggle, active) { 2079 if (!toggle) return; 2080 toggle.classList.toggle('fp-active', !!active); 2081 } 2082 2083 function setupDropdownMenus() { 2084 const isMob = isMobile() 2085 document.querySelectorAll('.fp-dropdown-toggle').forEach(toggle => { 2086 // åºåç§»å¨ç«¯åpc端ï¼ç§»å¨ç«¯éç¨touchstartäºä»¶ï¼pc端使ç¨mouseenter + click 2087 if (!isMob) { 2088 toggle.addEventListener('mouseenter', function(e) { 2089 e.preventDefault(); 2090 e.stopPropagation(); 2091 2092 const menu = getDropdownMenuForToggle(this); 2093 if (!menu) return; 2094 const isShowing = menu.classList.contains('fp-show'); 2095 2096 closeAllDropdowns(menu); 2097 2098 if (!isShowing) { 2099 menu.classList.add('fp-show'); 2100 setDropdownToggleActive(this, true); 2101 } 2102 }); 2103 2104 toggle.addEventListener('click', function(e) { 2105 e.preventDefault(); 2106 e.stopPropagation(); 2107 2108 const menu = getDropdownMenuForToggle(this); 2109 if (!menu) return; 2110 const isShowing = menu.classList.contains('fp-show'); 2111 2112 closeAllDropdowns(); 2113 document.querySelectorAll('.fp-dropdown-toggle').forEach((t) => setDropdownToggleActive(t, false)); 2114 2115 if (!isShowing) { 2116 menu.classList.add('fp-show'); 2117 setDropdownToggleActive(this, true); 2118 } 2119 }); 2120 } else { 2121 toggle.addEventListener('touchstart', function(e) { 2122 e.preventDefault(); 2123 e.stopPropagation(); 2124 2125 const menu = this.nextElementSibling; 2126 const isShowing = menu.classList.contains('fp-show'); 2127 2128 closeAllDropdowns(); 2129 2130 if (!isShowing) { 2131 menu.classList.add('fp-show'); 2132 } 2133 }); 2134 } 2135 2136 }); 2137 2138 // ç»å®mouseleaveäºä»¶ï¼é¼ æ ç§»åºæ¶å ³éå½åèåï¼ä» PCï¼ 2139 if (!isMob) { 2140 document.querySelectorAll('.fp-nav-item').forEach(nav => { 2141 nav.addEventListener('mouseleave', function(e) { 2142 e.preventDefault(); 2143 e.stopPropagation(); 2144 2145 const menus = this.querySelectorAll('.fp-dropdown-menu'); 2146 menus.forEach(menu => { 2147 menu.classList.remove('fp-show'); 2148 }); 2149 this.querySelectorAll('.fp-dropdown-toggle').forEach((t) => setDropdownToggleActive(t, false)); 2150 }); 2151 }); 2152 } 2153 2154 document.addEventListener('click', e => { 2155 const menu = e.target.closest('.fp-dropdown-menu') 2156 closeAllDropdowns(menu) 2157 }); 2158 } 2159 2160 /* 2161 * targetMenu ä¼ å ¥çç®æ èåä¸ä¼å ³é 2162 */ 2163 function closeAllDropdowns(targeMenu) { 2164 document.querySelectorAll('.fp-dropdown-menu').forEach(menu => { 2165 if (!targeMenu || (targeMenu && menu !== targeMenu)) { 2166 menu.classList.remove('fp-show'); 2167 } 2168 }); 2169 } 2170 2171 async function handleLogout(e) { 2172 e.preventDefault(); 2173 localStorage.removeItem('token'); 2174 localStorage.removeItem('userInfo'); 2175 isAuthenticated = false;
2176 quickShareCache = null; 2177 closeQuickShareModal(); 2178 try { 2179 await fetch(CONFIG.logoutUrl, { 2180 method: 'POST', 2181 credentials: 'include' 2182 }); 2183 } catch (error) { 2184 console.log('Logout request failed:', error); 2185 } 2186 crispReset(); 2187 window.location.assign(`/${getcurrentLanguage()}`); 2188 } 2189 2190 function createWelcomeModal(title, points, btnText, btnUrl) { 2191 const resources = dataResource.i18nResources[currentLanguage] || dataResource.i18nResources.en; 2192 // å建é®ç½©å± 2193 const overlay = document.createElement('div'); 2194 Object.assign(overlay.style, { 2195 position: 'fixed', 2196 top: '0', 2197 left: '0', 2198 right: '0', 2199 bottom: '0', 2200 background: 'rgba(0, 0, 0, 0.5)', 2201 display: 'flex', 2202 justifyContent: 'center', 2203 alignItems: 'center', 2204 zIndex: '1000', 2205 opacity: '0', 2206 transition: 'opacity 0.3s ease-in-out', 2207 backdropFilter: 'blur(5px)' 2208 }); 2209 2210 // å建弹çªå®¹å¨ 2211 const welcomeModal = document.createElement('div'); 2212 Object.assign(welcomeModal.style, { 2213 borderRadius: '20px', 2214 overflow: 'hidden', 2215 width: '90%', 2216 maxWidth: '500px', 2217 display: 'flex', 2218 flexDirection: 'column', 2219 maxHeight: '90vh', 2220 position: 'relative', 2221 transform: 'scale(0.9)', 2222 opacity: '1', 2223 color: '#333' 2224 }); 2225 overlay.appendChild(welcomeModal); 2226 2227 // å建头é¨å 容 2228 const modalHeader = document.createElement('div'); 2229 Object.assign(modalHeader.style, { 2230 padding: '0px 0px 10px', 2231 textAlign: 'center', 2232 position: 'relative', 2233 overflow: 'hidden' 2234 }); 2235 welcomeModal.appendChild(modalHeader); 2236 2237 // åå»ºå¤´é¨æ é¢ 2238 const modalHeaderTitle = document.createElement('div'); 2239 modalHeaderTitle.textContent = resources.nav.register.couponWelcome; 2240 modalHeaderTitle.style.cssText = ` 2241 font-size: 24px; 2242 font-weight: bold; 2243 background: linear-gradient(90deg, rgb(232 182 73) 0%, rgb(103 191 187) 100%) text; 2244 -webkit-background-clip: text; 2245 -webkit-text-fill-color: transparent; 2246 `; 2247 2248 modalHeader.appendChild(modalHeaderTitle); 2249 2250 // å建å å®¹åº 2251 const modalContent = document.createElement('div'); 2252 Object.assign(modalContent.style, { 2253 display: 'flex', 2254 position: 'relative' 2255 }); 2256 welcomeModal.appendChild(modalContent); 2257 2258 // å建å 容åºä¼æ å¸ 2259 const modalContentcoupon = document.createElement('div'); 2260 modalContentcoupon.style.cssText = ` 2261 flex: 1; 2262 background: url(${couponBackgroundUrl}) no-repeat; 2263 position: relative; 2264 height: 200px; 2265 background-size: 100% 100%; 2266 display: flex; 2267 `; 2268 2269 // å建å 容åºä¼æ å¸å·¦ä¾§ 2270 const couponLeft = document.createElement('div'); 2271 couponLeft.style.cssText = ` 2272 width: 25%; 2273 height: 100%; 2274 padding-left: 20px; 2275 box-sizing: border-box; 2276 text-align: center; 2277 `; 2278 2279 // å建å 容åºä¼æ å¸å·¦ä¾§logo 2280 const couponLeftLogo = document.createElement('div'); 2281 couponLeftLogo.style.cssText = ` 2282 height: 80px; 2283 position: relative; 2284 `; 2285 // åå»ºè£ é¥°å ç´ ï¼çº¯Unicodeåç¬¦ï¼ 2286 const couponLeftLogoDecoration = document.createElement('span'); 2287 couponLeftLogoDecoration.textContent = 'ð'; // 礼ç©ç¬¦å· 2288 Object.assign(couponLeftLogoDecoration.style, { 2289 position: 'absolute', 2290 top: '25px', 2291 right: '0', 2292 left: '0', 2293 fontSize: '40px', 2294 color: '#f47c20', 2295 transform: 'rotate(15deg)', 2296 zIndex: '1', 2297 animation: 'floatIcon 3s ease-in-out infinite' 2298 }); 2299 couponLeftLogo.appendChild(couponLeftLogoDecoration); 2300 2301 // å建å 容åºä¼æ 叿°å¼ 2302 const couponLeftValue = document.createElement('div'); 2303 couponLeftValue.textContent = points; 2304 couponLeftValue.style.cssText = ` 2305 font-size: 40px; 2306 color: #384020; 2307 `; 2308 2309 // å建å 容åºä¼æ 叿°å¼åä½ 2310 const couponLeftValueUnit = document.createElement('div'); 2311 couponLeftValueUnit.textContent = resources.nav.register.couponValueUnit; 2312 couponLeftValueUnit.style.cssText = `
2313 font-size: 20px; 2314 `; 2315 2316 couponLeft.appendChild(couponLeftLogo); 2317 couponLeft.appendChild(couponLeftValue); 2318 couponLeft.appendChild(couponLeftValueUnit); 2319 2320 // å建å 容åºä¼æ å¸å³ä¾§ 2321 const couponRight = document.createElement('div'); 2322 couponRight.style.cssText = ` 2323 flex: 1; 2324 height: 100%; 2325 padding-right: 20px; 2326 box-sizing: border-box; 2327 text-align: center; 2328 padding: 10px 0; 2329 `; 2330 2331 const couponRightTitle = document.createElement('div'); 2332 couponRightTitle.textContent = title; 2333 couponRightTitle.style.cssText = ` 2334 font-size: 18px; 2335 padding: 10px 20px 0; 2336 height: 30px; 2337 vertical-align: bottom; 2338 line-height: 1.1; 2339 `; 2340 2341 const couponRightValue = document.createElement('div'); 2342 couponRightValue.textContent = `${resources.nav.register.couponValueText} $${Number(points)/100}`; 2343 couponRightValue.style.cssText = ` 2344 font-size: 40px; 2345 font-weight: bold; 2346 color: #384020; 2347 `; 2348 2349 const couponRightTimer = document.createElement('div'); 2350 couponRightTimer.textContent = ''; 2351 couponRightTimer.style.cssText = ` 2352 padding: 10px; 2353 `; 2354 2355 const couponRightTimerMinute = document.createElement('span'); 2356 couponRightTimerMinute.id = 'popup-coupon-minute'; 2357 couponRightTimerMinute.textContent = '30';
2358 couponRightTimerMinute.style.cssText = ` 2359 padding: 5px 8px; 2360 background-color: red; 2361 color: #fff; 2362 border-radius: 5px; 2363 display: inline-block; 2364 `; 2365 2366 const couponRightTimerColon = document.createElement('span'); 2367 couponRightTimerColon.textContent = ':'; 2368 couponRightTimerColon.style.cssText = ` 2369 padding: 0 10px; 2370 font-weight: bold; 2371 `; 2372 2373 const couponRightTimerSecond = document.createElement('span'); 2374 couponRightTimerSecond.id = 'popup-coupon-second'; 2375 couponRightTimerSecond.textContent = '00'; 2376 couponRightTimerSecond.style.cssText = ` 2377 padding: 5px 8px; 2378 background-color: red; 2379 color: #fff; 2380 border-radius: 5px; 2381 display: inline-block; 2382 `; 2383 2384 couponRightTimer.appendChild(couponRightTimerMinute); 2385 couponRightTimer.appendChild(couponRightTimerColon); 2386 couponRightTimer.appendChild(couponRightTimerSecond); 2387 2388 const couponRightBottom = document.createElement('div'); 2389 couponRightBottom.textContent = resources.nav.register.couponTimeText; 2390 couponRightBottom.style.cssText = ``; 2391 2392 couponRight.appendChild(couponRightTitle); 2393 couponRight.appendChild(couponRightValue); 2394 couponRight.appendChild(couponRightTimer); 2395 couponRight.appendChild(couponRightBottom); 2396 2397 modalContentcoupon.appendChild(couponLeft); 2398 modalContentcoupon.appendChild(couponRight); 2399 2400 modalContent.appendChild(modalContentcoupon) 2401 2402 // å建æé® 2403 const modalButtons = document.createElement('div'); 2404 Object.assign(modalButtons.style, { 2405 display: 'flex', 2406 justifyContent: 'center', 2407 gap: '15px', 2408 flexDirection: 'column', 2409 alignItems: 'center' 2410 }); 2411 welcomeModal.appendChild(modalButtons); 2412 2413 const registBtn = document.createElement('button'); 2414 registBtn.addEventListener('click', () => { 2415 reportLog('coupon popup', 'register button click', {}); 2416 if (overlay) { 2417 overlay.remove(); 2418 stopTimerCount(); 2419 // sessionStorage.setItem('popupShown', 'true'); 2420 } 2421 window.location.href = btnUrl; 2422 }) 2423 registBtn.innerHTML = `ð ${btnText}`; 2424 registBtn.style.cssText = ` 2425 box-sizing: border-box; 2426 margin-top: 20px; 2427 padding: 8px 35px; 2428 border-radius: 10px; 2429 font-weight: 600; 2430 cursor: pointer; 2431 transition: 0.3s; 2432 border: none; 2433 font-size: 16px; 2434 min-width: 140px; 2435 background: linear-gradient(90deg, rgb(73 113 232) 0%, rgb(255 184 51) 100%); 2436 color: white; 2437 `; 2438 modalButtons.appendChild(registBtn); 2439 2440 // åå»ºå ³éæé® 2441 const modalContentClose = document.createElement('div'); 2442 modalContentClose.textContent = 'Ã'; 2443 modalContentClose.style.cssText = ` 2444 cursor: pointer; 2445 margin-left: 5px; 2446 width: 40px; 2447 height: 40px; 2448 text-align: center; 2449 line-height: 30px; 2450 font-size: 28px; 2451 border: 1px solid rgb(187, 186, 186); 2452 border-radius: 20px; 2453 color: rgb(187, 187, 187); 2454 box-sizing: border-box; 2455 margin-top: 20px; 2456 `; 2457 modalContentClose.addEventListener('click', () => { 2458 if (overlay) { 2459 reportLog('coupon popup', 'close button click', {}); 2460 overlay.remove(); 2461 stopTimerCount(); 2462 // sessionStorage.setItem('popupShown', 'true'); 2463 } 2464 }) 2465 2466 modalButtons.appendChild(modalContentClose); 2467 2468 document.body.appendChild(overlay); 2469 2470 // åå»ºæ ·å¼ 2471 const style = document.createElement('style'); 2472 style.textContent = ` 2473 @keyframes modalIn { 2474 0% { transform: translateY(-50px) scale(0.8); opacity: 0; } 2475 100% { transform: translateY(0) scale(1); opacity: 1; } 2476 } 2477 @keyframes pulse { 2478 0% { opacity: 0.7; transform: translate(-50%, -50%) scale(0.9); } 2479 50% { opacity: 0.9; transform: translate(-50%, -50%) scale(1.1); } 2480 100% { opacity: 0.7; transform: translate(-50%, -50%) scale(0.9); } 2481 } 2482 @keyframes floatIcon { 2483 0%, 100% { transform: translateY(0) rotate(-15deg); } 2484 50% { transform: translateY(-15px) rotate(15deg); } 2485 } 2486 `; 2487 document.head.appendChild(style); 2488 stratTimerCount(overlay); 2489 2490 return overlay; 2491 2492 } 2493
2494 let couponCountTimer = null; 2495 function stratTimerCount(overlay) { 2496 // åè®¡æ¶æ»ç§æ°ï¼30åé = 1800ç§ï¼ 2497 let totalSeconds = 30 * 60; 2498 2499 function updateCountdown() { 2500 const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0'); 2501 const seconds = String(totalSeconds % 60).padStart(2, '0'); 2502 document.getElementById("popup-coupon-minute").textContent = minutes; 2503 document.getElementById("popup-coupon-second").textContent = seconds; 2504 2505 if (totalSeconds <= 0) { 2506 clearInterval(couponCountTimer); 2507 overlay.remove(); 2508 } 2509 2510 totalSeconds--; 2511 } 2512 2513 // å æ§è¡ä¸æ¬¡ï¼é²æ¢é¡µé¢åæå¼æ¶å»¶è¿1ç§ 2514 updateCountdown(); 2515 2516 // æ¯ç§æ´æ°ä¸æ¬¡ 2517 couponCountTimer = setInterval(updateCountdown, 1000); 2518 } 2519 2520 function stopTimerCount() { 2521 clearInterval(couponCountTimer); 2522 } 2523 2524 const isAllowedPath = (p = location.pathname) => 2525 /^\/(?:$|([A-Za-z]{2})(?:\/(?:$|products\/[^\/]+\/?)?)?)$/i.test(p); 2526 const couponBackgroundUrl = `https://fronted.familypro.io/${window.location.href.includes('familypro') && !window.location.href.includes('dev.familypro') ? 'familypro' : 'familypro-dev'}/fronted/sections/images/coupon.webp`; 2527 // æ¾ç¤ºå¼¹çªç彿° 2528 function showWelcomeDialog(title, points, btnText, acceptUrl) { 2529 2530 // åªæé¦é¡µå详æ 页å è®¸å¼¹çª 2531 if (!isAllowedPath()) { 2532 return; 2533 } 2534 2535 2536 // å»¶è¿10ç§å±ç¤ºä¼æ ä¿¡æ¯ï¼æä¸ä½¿ç¨ 2537 // setTimeout(function () { 2538 // const img = new Image(); 2539 // img.src = couponBackgroundUrl; 2540 // img.onload = () => { 2541 // const overlay = createWelcomeModal(title, points, btnText, acceptUrl); 2542 2543 // const startOfToday = new Date(new Date().setHours(0, 0, 0, 0)).getTime(); 2544 // sessionStorage.setItem('popupShown', startOfToday); 2545 2546 // // æ·¡å ¥æ¾ç¤º 2547 // setTimeout(() => { 2548 // overlay.style.opacity = '1'; 2549 // const modal = overlay.querySelector('div'); 2550 // modal.style.animation = 'modalIn 0.6s forwards cubic-bezier(0.175, 0.885, 0.32, 1.275)'; 2551 // }, 10); 2552 // } 2553 // }, 10000) 2554 } 2555 2556 function createPromoDialog() { 2557 const urlParams = new URLSearchParams(window.location.search || ''); 2558 const promo = urlParams.get('promo'); 2559 2560 const texts = getPromoTexts(); 2561 const overlay = document.createElement('div'); 2562 Object.assign(overlay.style, { 2563 position: 'fixed', 2564 inset: '0', 2565 background: 'rgba(0,0,0,0.55)', 2566 display: 'flex', 2567 justifyContent: 'center', 2568 alignItems: 'center', 2569 zIndex: '1000' 2570 }); 2571 2572 const modal = document.createElement('div'); 2573 Object.assign(modal.style, { 2574 width: '92%', 2575 maxWidth: '420px', 2576 background: '#ffffff', 2577 borderRadius: '18px', 2578 padding: '26px 26px 22px', 2579 boxShadow: '0 12px 30px rgba(15,23,42,0.30)', 2580 position: 'relative', 2581 boxSizing: 'border-box', 2582 textAlign: 'center' 2583 }); 2584 2585 const closeBtn = document.createElement('button'); 2586 closeBtn.textContent = 'Ã'; 2587 Object.assign(closeBtn.style, { 2588 position: 'absolute', 2589 right: '16px', 2590 top: '14px', 2591 border: 'none', 2592 background: 'transparent', 2593 fontSize: '20px', 2594 cursor: 'pointer', 2595 color: '#A0AEC0' 2596 }); 2597 2598 const headerRow = document.createElement('div'); 2599 Object.assign(headerRow.style, { 2600 display: 'flex', 2601 alignItems: 'center', 2602 justifyContent: 'flex-start', 2603 marginBottom: '12px', 2604 textAlign: 'left' 2605 }); 2606 2607 const iconWrap = document.createElement('div'); 2608 Object.assign(iconWrap.style, { 2609 width: '56px', 2610 height: '56px', 2611 borderRadius: '50%', 2612 background: '#FF7A1A', 2613 display: 'flex', 2614 alignItems: 'center', 2615 justifyContent: 'center', 2616 marginRight: '16px', 2617 boxShadow: '0 8px 18px rgba(255,122,26,0.45)', 2618 color: '#fff', 2619 fontSize: '28px', 2620 flexShrink: '0' 2621 }); 2622 iconWrap.textContent = 'ð'; 2623 2624 const headerTextWrap = document.createElement('div'); 2625 Object.assign(headerTextWrap.style, { 2626 display: 'flex', 2627 flexDirection: 'column', 2628 alignItems: 'flex-start', 2629 justifyContent: 'center' 2630 }); 2631 2632 const badge = document.createElement('div'); 2633 badge.textContent = texts.promoBadge || 'KOL'; 2634 Object.assign(badge.style, { 2635 display: 'inline-flex', 2636 alignItems: 'center', 2637 justifyContent: 'center', 2638 padding: '4px 12px', 2639 borderRadius: '999px', 2640 background: '#FFE7D3', 2641 color: '#C05621', 2642 fontSize: '12px', 2643 fontWeight: '600', 2644 marginBottom: '6px' 2645 }); 2646 2647 const title = document.createElement('div'); 2648 title.textContent = texts.promoDialogTitle || ''; 2649 Object.assign(title.style, { 2650 fontSize: '20px', 2651 fontWeight: '700', 2652 color: '#1A202C' 2653 }); 2654 2655 headerTextWrap.appendChild(badge); 2656 headerTextWrap.appendChild(title); 2657 headerRow.appendChild(iconWrap); 2658 headerRow.appendChild(headerTextWrap); 2659 2660 const desc = document.createElement('div'); 2661 const rawDesc = texts.promoDialogDesc || ''; 2662 desc.innerHTML = rawDesc; 2663 Object.assign(desc.style, { 2664 fontSize: '14px', 2665 color: '#4A5568', 2666 lineHeight: '1.7', 2667 marginBottom: '22px', 2668 textAlign: 'left' 2669 }); 2670 2671 const primaryBtn = document.createElement('button'); 2672 primaryBtn.textContent = texts.promoDialogPrimary || ''; 2673 Object.assign(primaryBtn.style, { 2674 width: '100%', 2675 padding: '11px 16px', 2676 borderRadius: '999px', 2677 border: 'none', 2678 background: '#FF6B00', 2679 color: '#fff', 2680 fontSize: '15px', 2681 fontWeight: '600', 2682 cursor: 'pointer', 2683 marginBottom: '10px' 2684 }); 2685 2686 const secondaryBtn = document.createElement('button'); 2687 secondaryBtn.textContent = texts.promoDialogSecondary || ''; 2688 Object.assign(secondaryBtn.style, { 2689 width: '100%', 2690 padding: '10px 16px', 2691 borderRadius: '999px', 2692 border: '1px solid #E2E8F0', 2693 background: '#F7FAFC', 2694 color: '#4A5568', 2695 fontSize: '14px', 2696 cursor: 'pointer', 2697 marginBottom: '14px' 2698 }); 2699 2700 // const footer = document.createElement('div'); 2701 // footer.textContent = texts.promoDialogHasAccount || ''; 2702 // Object.assign(footer.style, { 2703 // fontSize: '12px', 2704 // color: '#A0AEC0', 2705 // textAlign: 'center' 2706 // }); 2707 2708 const closeOverlay = () => { 2709 if (overlay && overlay.parentNode) { 2710 overlay.parentNode.removeChild(overlay); 2711 } 2712 }; 2713 2714 // ç¹å»èçä¸åç´æ¥å ³éå¼¹çªï¼é¿å 误触 2715 closeBtn.addEventListener('click', () => { 2716 reportLog('Promo_Popup_Close', 'KOL_Promo', { 2717 url: window.location.href, 2718 source: promo 2719 }); 2720 closeOverlay(); 2721 }); 2722 secondaryBtn.addEventListener('click', () => { 2723 reportLog('Promo_Popup_Login', 'KOL_Promo', { 2724 url: window.location.href, 2725 source: promo 2726 }); 2727 closeOverlay(); 2728 openPromoLoginFlow(); 2729 }); 2730 primaryBtn.addEventListener('click', () => { 2731 reportLog('Promo_Popup_Register', 'KOL_Promo', { 2732 url: window.location.href, 2733 source: promo 2734 }); 2735 closeOverlay(); 2736 openPromoLoginFlow(); 2737 }); 2738 2739 modal.appendChild(closeBtn); 2740 modal.appendChild(headerRow); 2741 modal.appendChild(desc); 2742 modal.appendChild(primaryBtn); 2743 modal.appendChild(secondaryBtn); 2744 // modal.appendChild(footer); 2745 overlay.appendChild(modal); 2746 return overlay; 2747 } 2748 2749 function maybeShowPromoDialogForGuest() { 2750 if (isAuthenticated) return; 2751 if (!isPromoValidated) return;
2752 const urlParams = new URLSearchParams(window.location.search || ''); 2753 const promo = urlParams.get('promo'); 2754 if (!promo) return; 2755 reportLog('Promo_Popup_Show', 'KOL_Promo', { 2756 url: window.location.href, 2757 source: promo 2758 }); 2759 const overlay = createPromoDialog(); 2760 document.body.appendChild(overlay); 2761 } 2762 2763 // å ³éå¼¹çª 2764 function closeWelcomeDialog() { 2765 const overlay = document.getElementById('overlay'); 2766 if (overlay) { 2767 const modal = overlay.querySelector('div'); 2768 modal.style.animation = 'modalIn 0.3s reverse forwards'; 2769 overlay.style.opacity = '0'; 2770 2771 setTimeout(() => { 2772 overlay.remove(); 2773 }, 300); 2774 } 2775 } 2776 2777 // ç§»å¨ç«¯å¤æ 2778 function isMobile() { 2779 return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 2780 } 2781 2782 function reportLog(objectName, eventName, properties) { 2783 let userInfo = null 2784 if (localStorage.getItem('userInfo')) { 2785 userInfo = JSON.parse(localStorage.getItem('userInfo')) 2786 } 2787 const logBase = { 2788 "app_name": window.location.href.includes('dev.familypro') ? 'dev.familypro' : window.location.href.includes('familypro') ? 'familypro' : 'dev.familypro', 2789 "user_id": userInfo?.user_id || null, 2790 "user_email": userInfo?.email || null, 2791 "visitor_id": getVisitorId(), 2792 "browser_type": navigator.userAgent, 2793 "screen_width": window.screen.width, 2794 "screen_height": window.screen.height, 2795 "language": navigator.language, 2796 "user_agent": navigator.userAgent, 2797 "platform": navigator.platform, 2798 "timezone": Intl.DateTimeFormat().resolvedOptions().timeZone, 2799 "referrer": document.referrer, 2800 "debug": window.location.href.includes('dev.familypro') ? true : window.location.href.includes('familypro') ? false : true, 2801 }; 2802 fetch('/api/logs/upload', { 2803 method: 'POST', 2804 headers: { 2805 'Content-Type': 'application/json', 2806 }, 2807 body: JSON.stringify({ 2808 events: [{ 2809 eventBase: { 2810 eventLocation: window.location.pathname, 2811 eventName: eventName, 2812 objectName: objectName, 2813 logBase 2814 }, 2815 properties: properties || {} 2816 }] 2817 }) 2818 }) 2819 } 2820 2821 // ç®åè½»éç访客æ è¯çæå¨ 2822 function getVisitorId() { 2823 // å°è¯ä»æ¬å°ç¼å读å 2824 let id = localStorage.getItem('visitorId') 2825 if (id) return id 2826 2827 // åºäºæµè§å¨ç¹å¾çæï¼è½»éï¼ 2828 const ua = navigator.userAgent || '' 2829 const lang = navigator.language || '' 2830 const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || '' 2831 const screenInfo = `${screen.width}x${screen.height}x${screen.colorDepth}` 2832 2833 const platform = navigator.platform || '' 2834 const hardware = navigator.hardwareConcurrency || '' 2835 const maxTouch = navigator.maxTouchPoints || '' 2836 const raw = ua + lang + tz + screenInfo + platform + hardware + maxTouch 2837 2838 // è½»é hash ç®æ³ï¼MurmurHash-like 2839 function simpleHash(str) { 2840 let h1 = 0xdeadbeef 2841 for (let i = 0; i < str.length; i++) { 2842 h1 = Math.imul(h1 ^ str.charCodeAt(i), 2654435761) 2843 } 2844 h1 = (h1 ^ (h1 >>> 16)) >>> 0 2845 return ('00000000' + h1.toString(16)).slice(-8) 2846 } 2847 2848 // çææ´é¿ IDï¼æ¼æ¥ä¸¤æ®µ hashï¼ 2849 const mid = Math.floor(raw.length / 2) 2850 const hash = simpleHash(raw.slice(0, mid)) + simpleHash(raw.slice(mid)) 2851 id = 'v-' + hash // ä¾å¦ v-5e3f8a6c2c4d8f2b 2852 2853 localStorage.setItem('visitorId', id) 2854 return id 2855 } 2856 2857 // Register page load event logging 2858 const logPageLoad = () => { 2859 // Calculate page load time using Performance API 2860 let loadTime = 0; 2861 try { 2862 const navigation = performance.getEntriesByType("navigation")[0]; 2863 if (navigation && navigation.loadEventEnd > 0) { 2864 // loadEventEnd must be greater than 0 to ensure page has loaded 2865 loadTime = Math.round(navigation.loadEventEnd - navigation.fetchStart); 2866 } else if (performance.timing) { 2867 // Fallback for older browsers 2868 const timing = performance.timing; 2869 if (timing.loadEventEnd > 0) { 2870 loadTime = Math.round(timing.loadEventEnd - timing.navigationStart); 2871 } else if (timing.domContentLoadedEventEnd > 0) { 2872 // Use domContentLoadedEventEnd as fallback if loadEventEnd is not available 2873 loadTime = Math.round(timing.domContentLoadedEventEnd - timing.navigationStart); 2874 } 2875 } 2876 // Ensure loadTime is not negative 2877 if (loadTime < 0) { 2878 loadTime = 0; 2879 } 2880 } catch (e) { 2881 // Performance API not available, use 0 2882 console.warn("[LogUtils] Failed to calculate load time:", e); 2883 } 2884 2885 reportLog("page", "onload", { 2886 path: window.location.pathname, 2887 search: window.location.search, 2888 hash: window.location.hash, 2889 full_url: window.location.href, 2890 referrer: document.referrer, 2891 load_time: loadTime, 2892 screen_width: window.screen.width, 2893 screen_height: window.screen.height, 2894 viewport_width: window.innerWidth, 2895 viewport_height: window.innerHeight, 2896 }); 2897 }; 2898 2899 // Log page load immediately if already loaded, otherwise wait for load event 2900 if (document.readyState === "complete") { 2901 // Page already loaded, log immediately 2902 setTimeout(logPageLoad, 0); 2903 } else { 2904 // Wait for page to load 2905 window.addEventListener("load", logPageLoad, { once: true }); 2906 } 2907 2908 // Start the initialization 2909 initializeHeader(); 2910 }); 2911})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.