PageSourceSearch

https://fronted.familypro.io/familypro/fronted/sections/header.js?v=269301023

js familypro.io collected 2026-10-01 15:45:11 UTC 103,892 bytes, 2,911 lines download raw bytes

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, '&amp;')
430        .replace(/</g, '&lt;')
431        .replace(/>/g, '&gt;')
432        .replace(/"/g, '&quot;');
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 = '&times;';
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.