pb-sxp-ui 1.20.62 → 1.20.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -898,6 +898,7 @@ const SxpDataSourceProvider = ({ render, dataSources, utmVal, enableReportEvent
898
898
  const popupCurTimeRef = React.useRef(null);
899
899
  const [isNoMoreData, setIsNoMoreData] = React.useState(false);
900
900
  const [globalConfig, setGlobalConfig] = React.useState((_b = (_a = data === null || data === void 0 ? void 0 : data.data) === null || _a === void 0 ? void 0 : _a.sxpPageConf) === null || _b === void 0 ? void 0 : _b.globalConfig);
901
+ const [shopifyConfig, setShopifyConfig] = React.useState();
901
902
  const [pageData, setPageData] = React.useState();
902
903
  const [showConsent, setShowConsent] = React.useState(false);
903
904
  const [layoutVariantId, setLayoutVariantId] = React.useState();
@@ -1799,25 +1800,31 @@ const SxpDataSourceProvider = ({ render, dataSources, utmVal, enableReportEvent
1799
1800
  let pbType;
1800
1801
  getRecommendVideos()
1801
1802
  .then((data) => {
1802
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
1803
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
1803
1804
  if (data) {
1805
+ if ((_a = data.shopify) === null || _a === void 0 ? void 0 : _a.domain) {
1806
+ setShopifyConfig({
1807
+ domain: data.shopify.domain,
1808
+ storefrontAccessToken: data.shopify.storefrontAccessToken
1809
+ });
1810
+ }
1804
1811
  const list = getFilterRecList(data);
1805
1812
  let curData;
1806
1813
  let gldata;
1807
1814
  if (data === null || data === void 0 ? void 0 : data.layoutVariantId) {
1808
1815
  layId = data === null || data === void 0 ? void 0 : data.layoutVariantId;
1809
1816
  setLayoutVariantId(data === null || data === void 0 ? void 0 : data.layoutVariantId);
1810
- const id = (_b = (_a = data === null || data === void 0 ? void 0 : data.layoutVariantId) === null || _a === void 0 ? void 0 : _a.split('-')) === null || _b === void 0 ? void 0 : _b[1];
1817
+ const id = (_c = (_b = data === null || data === void 0 ? void 0 : data.layoutVariantId) === null || _b === void 0 ? void 0 : _b.split('-')) === null || _c === void 0 ? void 0 : _c[1];
1811
1818
  if (id) {
1812
1819
  curData = dataList === null || dataList === void 0 ? void 0 : dataList.find((item) => (item === null || item === void 0 ? void 0 : item.id) === id);
1813
1820
  if (curData) {
1814
1821
  // 找到对应的变体页面配置
1815
1822
  setPageData(curData);
1816
- document.title = (_c = curData === null || curData === void 0 ? void 0 : curData.name) !== null && _c !== void 0 ? _c : 'home';
1817
- gldata = (_e = (_d = curData === null || curData === void 0 ? void 0 : curData.data) === null || _d === void 0 ? void 0 : _d.sxpPageConf) === null || _e === void 0 ? void 0 : _e.globalConfig;
1823
+ document.title = (_d = curData === null || curData === void 0 ? void 0 : curData.name) !== null && _d !== void 0 ? _d : 'home';
1824
+ gldata = (_f = (_e = curData === null || curData === void 0 ? void 0 : curData.data) === null || _e === void 0 ? void 0 : _e.sxpPageConf) === null || _f === void 0 ? void 0 : _f.globalConfig;
1818
1825
  setGlobalConfig(gldata);
1819
1826
  onUpdateSchema === null || onUpdateSchema === void 0 ? void 0 : onUpdateSchema(curData === null || curData === void 0 ? void 0 : curData.data);
1820
- if ((_j = (_h = (_g = (_f = gldata === null || gldata === void 0 ? void 0 : gldata.consent) === null || _f === void 0 ? void 0 : _f[0]) === null || _g === void 0 ? void 0 : _g.item) === null || _h === void 0 ? void 0 : _h.props) === null || _j === void 0 ? void 0 : _j.privacy_necessity)
1827
+ if ((_k = (_j = (_h = (_g = gldata === null || gldata === void 0 ? void 0 : gldata.consent) === null || _g === void 0 ? void 0 : _g[0]) === null || _h === void 0 ? void 0 : _h.item) === null || _j === void 0 ? void 0 : _j.props) === null || _k === void 0 ? void 0 : _k.privacy_necessity)
1821
1828
  setShowConsent(true);
1822
1829
  else
1823
1830
  setShowConsent(true);
@@ -1857,7 +1864,7 @@ const SxpDataSourceProvider = ({ render, dataSources, utmVal, enableReportEvent
1857
1864
  bffGetTagList(curData !== null && curData !== void 0 ? curData : finalPageData);
1858
1865
  if (channel) {
1859
1866
  const item = list === null || list === void 0 ? void 0 : list[0];
1860
- const traceInfo = ((_k = item === null || item === void 0 ? void 0 : item.video) === null || _k === void 0 ? void 0 : _k.traceInfo) || ((_l = item === null || item === void 0 ? void 0 : item.product) === null || _l === void 0 ? void 0 : _l.traceInfo) || '';
1867
+ const traceInfo = ((_l = item === null || item === void 0 ? void 0 : item.video) === null || _l === void 0 ? void 0 : _l.traceInfo) || ((_m = item === null || item === void 0 ? void 0 : item.product) === null || _m === void 0 ? void 0 : _m.traceInfo) || '';
1861
1868
  bffEventReport === null || bffEventReport === void 0 ? void 0 : bffEventReport({
1862
1869
  eventInfo: Object.assign({ eventSubject: 'multiPostClick', eventDescription: 'multiPostClick', traceInfo, branchfeed: channel }, (layId && { layoutVariantId: layId }))
1863
1870
  });
@@ -1882,7 +1889,14 @@ const SxpDataSourceProvider = ({ render, dataSources, utmVal, enableReportEvent
1882
1889
  bffGetTagList(data);
1883
1890
  getRecommendVideos()
1884
1891
  .then((data) => {
1892
+ var _a;
1885
1893
  if (data) {
1894
+ if ((_a = data.shopify) === null || _a === void 0 ? void 0 : _a.domain) {
1895
+ setShopifyConfig({
1896
+ domain: data.shopify.domain,
1897
+ storefrontAccessToken: data.shopify.storefrontAccessToken
1898
+ });
1899
+ }
1886
1900
  const list = getFilterRecList(data);
1887
1901
  if ((globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.playbook) === 'organic menu' && !channel) {
1888
1902
  list.unshift('organic menu');
@@ -1959,7 +1973,8 @@ const SxpDataSourceProvider = ({ render, dataSources, utmVal, enableReportEvent
1959
1973
  firstRtcList,
1960
1974
  pixelPvStatusRef,
1961
1975
  curReqInfo,
1962
- setCurReqInfo
1976
+ setCurReqInfo,
1977
+ shopifyConfig
1963
1978
  } }, isShowConsent ? (React.createElement(Consent$4, Object.assign({}, (_e = (_d = (_c = globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.consent) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.item) === null || _e === void 0 ? void 0 : _e.props))) : (render({
1964
1979
  rtcList,
1965
1980
  mutateLike: bffMutateLike,
@@ -2318,7 +2333,7 @@ function useEventReport() {
2318
2333
  * @returns { isVisible, reset } isVisible 是否可见;reset 重置可见状态(用于页面从后台恢复时重新触发检测)
2319
2334
  */
2320
2335
  function useAdvancedOnScreen(ref, options = {}) {
2321
- const { threshold = 0, delay = 0, onVisible, onHidden, name = 'unknown', deps = [] } = options;
2336
+ const { threshold = 0, delay = 0, onVisible, onHidden, name = 'unknown', deps = [], pageHideCountRef } = options;
2322
2337
  const observerRef = React.useRef(null);
2323
2338
  const [isVisible, setIsVisible] = React.useState(false);
2324
2339
  const timerRef = React.useRef(null);
@@ -2370,10 +2385,16 @@ function useAdvancedOnScreen(ref, options = {}) {
2370
2385
  clearTimer();
2371
2386
  }
2372
2387
  isWaitingRef.current = true;
2388
+ // 创建定时器时捕获当前隐藏版本号,回调时比对,防止 hide→show 竞态导致旧定时器上报
2389
+ const hideCount = pageHideCountRef === null || pageHideCountRef === void 0 ? void 0 : pageHideCountRef.current;
2373
2390
  timerRef.current = setTimeout(() => {
2374
2391
  var _a;
2375
2392
  isWaitingRef.current = false;
2376
2393
  timerRef.current = null;
2394
+ // 如果版本号变化,说明在定时器等待期间页面发生了 hide,丢弃本次回调
2395
+ if (pageHideCountRef && pageHideCountRef.current !== hideCount) {
2396
+ return;
2397
+ }
2377
2398
  (_a = onVisibleRef.current) === null || _a === void 0 ? void 0 : _a.call(onVisibleRef);
2378
2399
  }, delay);
2379
2400
  }
@@ -12331,7 +12352,7 @@ var settingRender$d = [
12331
12352
 
12332
12353
  const AddToCartPopup$1 = ({ isActive = true }) => {
12333
12354
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
12334
- const { popupDetailData, globalConfig, bffFbReport, bffEventReport } = useSxpDataSource();
12355
+ const { popupDetailData, globalConfig, bffFbReport, bffEventReport, popupCurTimeRef, shopifyConfig: contextShopifyConfig } = useSxpDataSource();
12335
12356
  const [productData, setProductData] = React.useState(null);
12336
12357
  const [selectedOptions, setSelectedOptions] = React.useState({});
12337
12358
  const [selectedVariant, setSelectedVariant] = React.useState(null);
@@ -12343,15 +12364,24 @@ const AddToCartPopup$1 = ({ isActive = true }) => {
12343
12364
  // 获取商品数据
12344
12365
  const data = popupDetailData;
12345
12366
  const product = (_e = (_b = (_a = data === null || data === void 0 ? void 0 : data.video) === null || _a === void 0 ? void 0 : _a.bindProduct) !== null && _b !== void 0 ? _b : (_d = (_c = data === null || data === void 0 ? void 0 : data.video) === null || _c === void 0 ? void 0 : _c.bindProducts) === null || _d === void 0 ? void 0 : _d[0]) !== null && _e !== void 0 ? _e : data === null || data === void 0 ? void 0 : data.product;
12346
- // Shopify 配置
12347
- const shopifyConfig = window.__SHOPIFY_CONFIG__;
12348
- const shopifyDomain = ((_f = globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.shopify) === null || _f === void 0 ? void 0 : _f.domain) || (globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.shopifyDomain) || (shopifyConfig === null || shopifyConfig === void 0 ? void 0 : shopifyConfig.domain) || 'dev-store-749237498237498636.myshopify.com';
12349
- const storefrontToken = ((_g = globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.shopify) === null || _g === void 0 ? void 0 : _g.storefrontAccessToken) || (globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.storefrontAccessToken) || (shopifyConfig === null || shopifyConfig === void 0 ? void 0 : shopifyConfig.storefrontAccessToken) || '77d894c490f79430ce7bd0a7efdff6b7';
12367
+ // Shopify 配置:优先取 list 接口返回的 shopify 字段(通过 context 注入),其次取 globalConfig 的静态配置
12368
+ const windowShopifyConfig = window.__SHOPIFY_CONFIG__;
12369
+ const shopifyDomain = (contextShopifyConfig === null || contextShopifyConfig === void 0 ? void 0 : contextShopifyConfig.domain) ||
12370
+ ((_f = globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.shopify) === null || _f === void 0 ? void 0 : _f.domain) ||
12371
+ (globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.shopifyDomain) ||
12372
+ (windowShopifyConfig === null || windowShopifyConfig === void 0 ? void 0 : windowShopifyConfig.domain) ||
12373
+ '';
12374
+ const storefrontToken = (contextShopifyConfig === null || contextShopifyConfig === void 0 ? void 0 : contextShopifyConfig.storefrontAccessToken) ||
12375
+ ((_g = globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.shopify) === null || _g === void 0 ? void 0 : _g.storefrontAccessToken) ||
12376
+ (globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.storefrontAccessToken) ||
12377
+ (windowShopifyConfig === null || windowShopifyConfig === void 0 ? void 0 : windowShopifyConfig.storefrontAccessToken) ||
12378
+ '';
12350
12379
  const productId = (product === null || product === void 0 ? void 0 : product.shopifyId) || (product === null || product === void 0 ? void 0 : product.itemId) || '';
12351
12380
  // 查询 Shopify 商品数据
12352
12381
  const fetchProductData = React.useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
12353
12382
  var _k;
12354
- if (!productId || !shopifyDomain || !storefrontToken) {
12383
+ // storefrontAccessToken 可为空(部分商店公开访问),只要 domain productId 存在即可发起请求
12384
+ if (!productId || !shopifyDomain) {
12355
12385
  setLoading(false);
12356
12386
  return;
12357
12387
  }
@@ -12399,12 +12429,13 @@ const AddToCartPopup$1 = ({ isActive = true }) => {
12399
12429
  }
12400
12430
  `;
12401
12431
  const fullProductId = productId.startsWith('gid://') ? productId : `gid://shopify/Product/${productId}`;
12432
+ const headers = { 'Content-Type': 'application/json' };
12433
+ if (storefrontToken) {
12434
+ headers['X-Shopify-Storefront-Access-Token'] = storefrontToken;
12435
+ }
12402
12436
  const response = yield fetch(`https://${shopifyDomain}/api/2024-01/graphql.json`, {
12403
12437
  method: 'POST',
12404
- headers: {
12405
- 'Content-Type': 'application/json',
12406
- 'X-Shopify-Storefront-Access-Token': storefrontToken
12407
- },
12438
+ headers,
12408
12439
  body: JSON.stringify({
12409
12440
  query,
12410
12441
  variables: { id: fullProductId }
@@ -12426,6 +12457,18 @@ const AddToCartPopup$1 = ({ isActive = true }) => {
12426
12457
  setLoading(false);
12427
12458
  }
12428
12459
  }), [productId, shopifyDomain, storefrontToken]);
12460
+ /**
12461
+ * 从商品弹窗打开加购弹窗时,用户在商品弹窗里的停留时长不应该继续累加。
12462
+ * 因此在加购弹窗关闭(组件卸载)时,将弹窗的起始时间重置为当前时间,
12463
+ * 这样后续关闭商品弹窗上报 productView 时,timeOnSite 只统计“返回商品弹窗之后”的浏览时长。
12464
+ */
12465
+ React.useEffect(() => {
12466
+ return () => {
12467
+ if (popupCurTimeRef) {
12468
+ popupCurTimeRef.current = new Date();
12469
+ }
12470
+ };
12471
+ }, [popupCurTimeRef]);
12429
12472
  React.useEffect(() => {
12430
12473
  if (isActive) {
12431
12474
  fetchProductData();
@@ -19600,54 +19643,35 @@ Object.values(_materials_$1).forEach((v) => {
19600
19643
  RESOLVER$3[v.extend.type] = v;
19601
19644
  }
19602
19645
  });
19603
- const CTAButton = ({ ctaData, style, onClick, onExposure, triggerKey }) => {
19646
+ const CTAButton = ({ ctaData, style, onClick, onExposure, triggerKey, isPageHiddenRef, pageHideCountRef }) => {
19604
19647
  const buttonRef = React.useRef(null);
19605
- const timerRef = React.useRef(null);
19606
- const lastVisibleStateRef = React.useRef(null); // 记录上一次的可见状态,null 表示首次渲染
19607
- const isFirstRenderRef = React.useRef(true); // 标记是否首次渲染
19608
- // 使用高级曝光检测:完全可见
19609
- // 注意:CTA 按钮使用 1.0 阈值,确保按钮完全进入可视区域才触发曝光
19610
- const { isVisible } = useAdvancedOnScreen(buttonRef, {
19611
- threshold: 1.0, // 完全可见
19612
- delay: 0, // 不使用内置延迟,在 useEffect 中处理
19613
- onVisible: undefined, // 不使用回调,改用状态
19614
- onHidden: undefined
19615
- });
19616
- // 监听可见性变化,只在 false → true 时触发上报
19648
+ const onExposureRef = React.useRef(onExposure);
19617
19649
  React.useEffect(() => {
19618
- // 清除之前的定时器
19619
- if (timerRef.current) {
19620
- clearTimeout(timerRef.current);
19621
- timerRef.current = null;
19622
- }
19623
- // 首次渲染时,初始化 lastVisibleStateRef 但不触发上报
19624
- // 等到下次状态变化时再判断
19625
- if (isFirstRenderRef.current) {
19626
- isFirstRenderRef.current = false;
19627
- lastVisibleStateRef.current = isVisible;
19628
- console.log(`[CTAButton] 首次渲染,初始状态: ${isVisible}`);
19629
- return;
19630
- }
19631
- // 只有从不可见变为可见时才触发上报(避免小幅滑动重复触发)
19632
- if (isVisible && lastVisibleStateRef.current === false) {
19633
- console.log('[CTAButton] 从不可见变为可见,1秒后触发上报');
19634
- timerRef.current = setTimeout(() => {
19635
- console.log('[CTAButton] 触发 onExposure');
19636
- onExposure();
19637
- }, 1000);
19638
- }
19639
- // 更新上一次的可见状态
19640
- lastVisibleStateRef.current = isVisible;
19641
- return () => {
19642
- if (timerRef.current) {
19643
- clearTimeout(timerRef.current);
19644
- timerRef.current = null;
19650
+ onExposureRef.current = onExposure;
19651
+ }, [onExposure]);
19652
+ // 使用 useAdvancedOnScreen 的内置 delay 机制处理曝光:
19653
+ // - threshold 0.8:按钮 80% 可见时开始计时,低于 0.64(0.8*0.8)时才认为离开
19654
+ // (使用 1.0 会导致 exitThreshold=0.8,上下轻微滑动时按钮不会退出可见状态,
19655
+ // 从而 onVisible 永远不会再次触发,导致重新停留时不上报)
19656
+ // - delay 1000:停留满 1s 才触发 onVisible 回调
19657
+ // - onVisible 每次从不可见→可见+停留满 1s 都会触发,支持重复上报
19658
+ useAdvancedOnScreen(buttonRef, {
19659
+ threshold: 0.8,
19660
+ delay: 1000,
19661
+ onVisible: () => {
19662
+ if (isPageHiddenRef.current) {
19663
+ console.log('[CTAButton] 页面已隐藏,取消上报 onExposure');
19664
+ return;
19645
19665
  }
19646
- };
19647
- }, [isVisible, onExposure]);
19666
+ console.log('[CTAButton] 触发 onExposure');
19667
+ onExposureRef.current();
19668
+ },
19669
+ pageHideCountRef,
19670
+ name: 'CTAButton'
19671
+ });
19648
19672
  return (React.createElement("button", { ref: buttonRef, style: style, onClick: onClick }, ctaData.title));
19649
19673
  };
19650
- const ProductView = ({ children, onExposure, onLeave }) => {
19674
+ const ProductView = ({ children, onExposure, onLeave, pageHideCountRef }) => {
19651
19675
  const productRef = React.useRef(null);
19652
19676
  const onExposureRef = React.useRef(onExposure);
19653
19677
  const onLeaveRef = React.useRef(onLeave);
@@ -19665,11 +19689,12 @@ const ProductView = ({ children, onExposure, onLeave }) => {
19665
19689
  onHidden: () => {
19666
19690
  var _a;
19667
19691
  (_a = onLeaveRef.current) === null || _a === void 0 ? void 0 : _a.call(onLeaveRef);
19668
- }
19692
+ },
19693
+ pageHideCountRef
19669
19694
  });
19670
19695
  return (React.createElement("div", { ref: productRef, style: { width: '100%' } }, children));
19671
19696
  };
19672
- const PostView = ({ children, videoRef, isVideo, onExposure, autoPlayTriggerRef, onVideoPlay, onVideoPause, allowAutoPlay = true, videoUrl }) => {
19697
+ const PostView = ({ children, videoRef, isVideo, onExposure, autoPlayTriggerRef, onVideoPlay, onVideoPause, allowAutoPlay = true, videoUrl, pageHideCountRef }) => {
19673
19698
  const postRef = React.useRef(null);
19674
19699
  // 使用高级曝光检测:2/3区域(threshold=0.67)+ 1000ms延迟
19675
19700
  useAdvancedOnScreen(postRef, {
@@ -19729,7 +19754,8 @@ const PostView = ({ children, videoRef, isVideo, onExposure, autoPlayTriggerRef,
19729
19754
  }
19730
19755
  }
19731
19756
  },
19732
- name: 'PostView(Hero)'
19757
+ name: 'PostView(Hero)',
19758
+ pageHideCountRef
19733
19759
  });
19734
19760
  return (React.createElement("div", { ref: postRef, style: { width: '100%', height: '100%' } }, children));
19735
19761
  };
@@ -20173,6 +20199,11 @@ const StructurePage = (_a) => {
20173
20199
  const pageInitializedRef = React.useRef(false);
20174
20200
  // 标记页面是否正在卸载(刷新/关闭),防止刷新时旧页面的 visibilitychange visible 误触发
20175
20201
  const pageUnloadingRef = React.useRef(false);
20202
+ // 标记页面当前是否处于隐藏状态(最小化/切换标签),用于阻止延迟上报在页面隐藏后触发
20203
+ const pageHiddenRef = React.useRef(false);
20204
+ // 页面隐藏计数器:每次 visibilitychange hidden 时递增,用于让旧定时器回调识别自己是否已过期。
20205
+ // 解决"停留不足 1s 缩小页面后立即打开,旧定时器仍在运行且 pageHiddenRef 已被重置为 false"的竞态问题。
20206
+ const pageHideCountRef = React.useRef(0);
20176
20207
  // 全局事件管理:用于追踪弹窗打开时间和状态
20177
20208
  const popupCurTimeRef = React.useRef(new Date());
20178
20209
  const currentPopupDataRef = React.useRef(null);
@@ -20279,16 +20310,18 @@ const StructurePage = (_a) => {
20279
20310
  product: null
20280
20311
  };
20281
20312
  // 上报 clickCta 事件
20282
- // productGridSection 区域的商品 position 统一传 0(与 ctaExposure 保持一致)
20283
- const clickCtaPosition = viewPosition === 'productGridSection' ? 0 : (sectionIndex !== undefined ? sectionIndex : 0);
20313
+ // heroSection 以外的区域 position 统一传 0(与 ctaExposure 保持一致)
20314
+ const clickCtaPosition = viewPosition === 'heroSection' ? (sectionIndex !== undefined ? sectionIndex : 0) : 0;
20284
20315
  ctaEvent === null || ctaEvent === void 0 ? void 0 : ctaEvent(Object.assign({ eventSubject: 'clickCta', eventDescription: 'User clicked the CTA' }, (viewPosition && { viewPosition })), rec, productData, clickCtaPosition);
20316
+ // heroSection 以外的区域 position 统一传 0
20317
+ const fbPosition = viewPosition === 'heroSection' ? (sectionIndex !== undefined ? sectionIndex : 0) : 0;
20285
20318
  // ========== 2. Facebook Pixel 和第三方像素上报 ==========
20286
20319
  bffFbReport === null || bffFbReport === void 0 ? void 0 : bffFbReport({
20287
20320
  eventName: 'ClickCTA',
20288
20321
  product: productData ? [productData] : undefined,
20289
20322
  contentType: 'post',
20290
20323
  rec: rec,
20291
- position: sectionIndex !== undefined ? sectionIndex : 0,
20324
+ position: fbPosition,
20292
20325
  cta_text: (ctaData === null || ctaData === void 0 ? void 0 : ctaData.title) || '',
20293
20326
  cta_action_type: isExternalLink ? 'open_external_link' : 'open_internal_popup',
20294
20327
  target_content_id: (productData === null || productData === void 0 ? void 0 : productData.itemId) || '',
@@ -20305,7 +20338,7 @@ const StructurePage = (_a) => {
20305
20338
  const popupNode = popupList.find((p) => p.id === popupType);
20306
20339
  const isAddToCartPopup = ((_c = popupNode === null || popupNode === void 0 ? void 0 : popupNode.item) === null || _c === void 0 ? void 0 : _c.type) === 'AddToCartPopup';
20307
20340
  if ((productData === null || productData === void 0 ? void 0 : productData.shopifyId) && isAddToCartPopup) {
20308
- ctaEvent === null || ctaEvent === void 0 ? void 0 : ctaEvent(Object.assign({ eventSubject: 'clickShopifyPopup', eventDescription: 'User clicked to open Shopify popup' }, (viewPosition && { viewPosition })), rec, productData, sectionIndex !== undefined ? sectionIndex : 0);
20341
+ ctaEvent === null || ctaEvent === void 0 ? void 0 : ctaEvent(Object.assign({ eventSubject: 'clickShopifyPopup', eventDescription: 'User clicked to open Shopify popup' }, (viewPosition && { viewPosition })), rec, productData, fbPosition);
20309
20342
  }
20310
20343
  // ========== 打开弹窗前:记录弹窗数据和时间 ==========
20311
20344
  // 重置弹窗时间
@@ -20335,7 +20368,7 @@ const StructurePage = (_a) => {
20335
20368
  product: productData ? [productData] : undefined,
20336
20369
  contentType: 'product',
20337
20370
  rec: recData,
20338
- position: sectionIndex !== undefined ? sectionIndex : 0
20371
+ position: fbPosition
20339
20372
  });
20340
20373
  // 上报 PageView 事件
20341
20374
  bffFbReport === null || bffFbReport === void 0 ? void 0 : bffFbReport({
@@ -20343,11 +20376,11 @@ const StructurePage = (_a) => {
20343
20376
  product: productData ? [productData] : undefined,
20344
20377
  contentType: 'product',
20345
20378
  rec: recData,
20346
- position: sectionIndex !== undefined ? sectionIndex : 0
20379
+ position: fbPosition
20347
20380
  });
20348
20381
  console.log('[StructurePage] 上报弹窗打开事件 (ProductView + PageView)', {
20349
20382
  productId: productData === null || productData === void 0 ? void 0 : productData.itemId,
20350
- position: sectionIndex !== undefined ? sectionIndex : 0
20383
+ position: fbPosition
20351
20384
  });
20352
20385
  // 设置弹窗要显示的产品数据
20353
20386
  if (productData && typeof window !== 'undefined' && window.setPopupDetailData) {
@@ -20459,6 +20492,11 @@ const StructurePage = (_a) => {
20459
20492
  }, [multiCTAConfig]);
20460
20493
  // CTA 曝光事件处理(允许重复上报)
20461
20494
  const handleCtaExposure = React.useCallback((ctaData, productData, viewPosition, sectionIndex, buttonKey) => {
20495
+ // 页面已隐藏(最小化/切换标签)时,不上报曝光事件,避免在 sessionCompleted 之后上报
20496
+ if (pageHiddenRef.current) {
20497
+ console.log(`[handleCtaExposure] 页面已隐藏,跳过 ctaExposure 上报`);
20498
+ return;
20499
+ }
20462
20500
  // 生成唯一标识(不包含 carouselIndex,确保 carousel 切换时不重复上报)
20463
20501
  const ctaId = `${buttonKey}-${viewPosition}`;
20464
20502
  // 更新曝光时间(允许重复上报,由 CTAButton 组件控制何时触发)
@@ -20492,9 +20530,9 @@ const StructurePage = (_a) => {
20492
20530
  product: null
20493
20531
  };
20494
20532
  // 确定上报的 position 参数
20495
- // productGridSection 的所有 CTA 曝光事件的 position 都传 0
20533
+ // productGridSection carouselSection 的 CTA 曝光事件 position 统一传 0
20496
20534
  let reportPosition = 0;
20497
- if (viewPosition === 'productGridSection') {
20535
+ if (viewPosition !== 'heroSection') {
20498
20536
  reportPosition = 0;
20499
20537
  }
20500
20538
  else {
@@ -20779,7 +20817,7 @@ const StructurePage = (_a) => {
20779
20817
  }
20780
20818
  }
20781
20819
  // 默认渲染按钮(包装一个 CTAButton 组件来处理曝光事件)
20782
- return (React.createElement(CTAButton, { ctaData: ctaData, style: mergeStyles(fallbackStyle || baseStyles.heroButton, buttonKey), onClick: () => handleCtaClick(ctaData === null || ctaData === void 0 ? void 0 : ctaData.link, interaction, productData, ctaData, viewPosition, sectionIndex), onExposure: () => handleCtaExposure(ctaData, productData, viewPosition, sectionIndex, buttonKey) }));
20820
+ return (React.createElement(CTAButton, { ctaData: ctaData, style: mergeStyles(fallbackStyle || baseStyles.heroButton, buttonKey), onClick: () => handleCtaClick(ctaData === null || ctaData === void 0 ? void 0 : ctaData.link, interaction, productData, ctaData, viewPosition, sectionIndex), onExposure: () => handleCtaExposure(ctaData, productData, viewPosition, sectionIndex, buttonKey), isPageHiddenRef: pageHiddenRef, pageHideCountRef: pageHideCountRef }));
20783
20821
  }, [multiCTAConfig, handleCtaClick, handleCtaExposure, mergeStyles, rest, carouselIndex]);
20784
20822
  // 监听页面卸载事件(刷新/关闭),设置 pageUnloadingRef 防止旧页面监听器误触发
20785
20823
  React.useEffect(() => {
@@ -21349,7 +21387,7 @@ const StructurePage = (_a) => {
21349
21387
  carouselImageStartTime.current = 0;
21350
21388
  }, [data, bffEventReport]);
21351
21389
  const handleCarouselPrev = () => {
21352
- var _a, _b, _c, _d;
21390
+ var _a;
21353
21391
  if (data === null || data === void 0 ? void 0 : data.carouselSection) {
21354
21392
  // 先上报当前图片的结束事件
21355
21393
  handleCarouselImageEndEvent(carouselIndex);
@@ -21402,20 +21440,10 @@ const StructurePage = (_a) => {
21402
21440
  carouselImagePendingReport.current[newIndex] = true;
21403
21441
  }
21404
21442
  }
21405
- // 如果 carousel 在视口内且新 slide 有 CTA,直接触发 CTA 曝光上报
21406
- if (carouselIsVisible.current) {
21407
- const newItem = carouselSection[newIndex];
21408
- const ctaData = (_c = (_b = newItem === null || newItem === void 0 ? void 0 : newItem.bindProducts) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.bindCta;
21409
- const productData = (_d = newItem === null || newItem === void 0 ? void 0 : newItem.bindProducts) === null || _d === void 0 ? void 0 : _d[0];
21410
- if (ctaData) {
21411
- console.log('[handleCarouselPrev] carousel 可见,触发新 slide CTA 曝光, index:', newIndex);
21412
- handleCtaExposure(ctaData, productData, 'carouselSection', newIndex, 'carouselButton');
21413
- }
21414
- }
21415
21443
  }
21416
21444
  };
21417
21445
  const handleCarouselNext = () => {
21418
- var _a, _b, _c, _d;
21446
+ var _a;
21419
21447
  if (data === null || data === void 0 ? void 0 : data.carouselSection) {
21420
21448
  // 先上报当前图片的结束事件
21421
21449
  handleCarouselImageEndEvent(carouselIndex);
@@ -21468,16 +21496,6 @@ const StructurePage = (_a) => {
21468
21496
  carouselImagePendingReport.current[newIndex] = true;
21469
21497
  }
21470
21498
  }
21471
- // 如果 carousel 在视口内且新 slide 有 CTA,直接触发 CTA 曝光上报
21472
- if (carouselIsVisible.current) {
21473
- const newItem = carouselSection[newIndex];
21474
- const ctaData = (_c = (_b = newItem === null || newItem === void 0 ? void 0 : newItem.bindProducts) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.bindCta;
21475
- const productData = (_d = newItem === null || newItem === void 0 ? void 0 : newItem.bindProducts) === null || _d === void 0 ? void 0 : _d[0];
21476
- if (ctaData) {
21477
- console.log('[handleCarouselNext] carousel 可见,触发新 slide CTA 曝光, index:', newIndex);
21478
- handleCtaExposure(ctaData, productData, 'carouselSection', newIndex, 'carouselButton');
21479
- }
21480
- }
21481
21499
  }
21482
21500
  };
21483
21501
  // Hero Section 的曝光检测 ref
@@ -21564,7 +21582,8 @@ const StructurePage = (_a) => {
21564
21582
  name: 'Hero',
21565
21583
  // data 加载完成后 heroSectionRef 才会挂载到 DOM,需要将 data 作为额外依赖
21566
21584
  // 确保 data 就绪后 IntersectionObserver 能重新初始化并正确观察元素
21567
- deps: [data]
21585
+ deps: [data],
21586
+ pageHideCountRef
21568
21587
  });
21569
21588
  // Carousel Section 的曝光检测 ref
21570
21589
  const carouselSectionRef = React.useRef(null);
@@ -21702,7 +21721,14 @@ const StructurePage = (_a) => {
21702
21721
  if (visibleTimer) {
21703
21722
  clearTimeout(visibleTimer);
21704
21723
  }
21724
+ // 创建定时器时捕获当前隐藏版本号,回调时比对,防止 hide→show 竞态导致旧定时器上报
21725
+ const hideCount = pageHideCountRef.current;
21705
21726
  visibleTimer = setTimeout(() => {
21727
+ // 版本号变化说明定时器等待期间页面发生了 hide,丢弃本次回调
21728
+ if (pageHideCountRef.current !== hideCount) {
21729
+ visibleTimer = null;
21730
+ return;
21731
+ }
21706
21732
  console.log('[Carousel Observer] 延迟时间到,触发 handleCarouselVisible');
21707
21733
  handleCarouselVisible();
21708
21734
  visibleTimer = null;
@@ -21894,6 +21920,11 @@ const StructurePage = (_a) => {
21894
21920
  if (document.visibilityState === 'hidden') {
21895
21921
  // ========== 页面隐藏时 ==========
21896
21922
  console.log('[StructurePage] Page hidden - 触发全局事件');
21923
+ // 标记页面已隐藏,阻止后续延迟上报(如 ctaExposure/productView 的 1s 防抖定时器)
21924
+ pageHiddenRef.current = true;
21925
+ // 递增隐藏版本号,使所有在本次隐藏前创建的延迟定时器在回调时能感知自己已过期,
21926
+ // 从而即使 pageHiddenRef 在页面重新可见后被重置为 false,这些旧定时器也不会上报事件。
21927
+ pageHideCountRef.current += 1;
21897
21928
  // 0. 上报当前可见的 post/product 的离开事件
21898
21929
  console.log('[visibilitychange] 页面隐藏,当前可见的 products 数量:', visibleProducts.current.length);
21899
21930
  // Hero Section: 上报离开事件
@@ -22074,6 +22105,8 @@ const StructurePage = (_a) => {
22074
22105
  else if (document.visibilityState === 'visible') {
22075
22106
  // ========== 页面可见时 ==========
22076
22107
  console.log('[StructurePage] Page visible - 触发全局事件');
22108
+ // 页面重新可见,重置隐藏标记,允许后续曝光事件正常上报
22109
+ pageHiddenRef.current = false;
22077
22110
  // 如果页面尚未完成首次加载(挂载时的 h5LinkEnterFeed 还未上报),
22078
22111
  // 则跳过此次 visibilitychange visible 处理,避免刷新时重复上报
22079
22112
  if (!pageInitializedRef.current) {
@@ -22417,7 +22450,7 @@ const StructurePage = (_a) => {
22417
22450
  return textStyle;
22418
22451
  })()) }, data.heroSection.text))),
22419
22452
  React.createElement("div", { style: baseStyles.heroImageContainer },
22420
- React.createElement(PostView, { videoRef: heroVideoRef, isVideo: !!data.heroSection.url, autoPlayTriggerRef: heroAutoPlayTrigger, allowAutoPlay: hasUserInteracted, videoUrl: data.heroSection.url || undefined, onExposure: () => {
22453
+ React.createElement(PostView, { videoRef: heroVideoRef, isVideo: !!data.heroSection.url, autoPlayTriggerRef: heroAutoPlayTrigger, allowAutoPlay: hasUserInteracted, videoUrl: data.heroSection.url || undefined, pageHideCountRef: pageHideCountRef, onExposure: () => {
22421
22454
  // 视频进入 2/3 可视区域并停留 1 秒后的曝光上报
22422
22455
  // 注意:不再主动调用 load(),让视频在自动播放时才开始加载
22423
22456
  // 这样可以避免在网络慢时提前加载视频
@@ -22608,7 +22641,7 @@ const StructurePage = (_a) => {
22608
22641
  set current(value) {
22609
22642
  carouselAutoPlayTriggers.current[index] = value;
22610
22643
  }
22611
- }, allowAutoPlay: hasUserInteracted, videoUrl: item.url, onExposure: () => {
22644
+ }, allowAutoPlay: hasUserInteracted, videoUrl: item.url, pageHideCountRef: pageHideCountRef, onExposure: () => {
22612
22645
  // 视频进入 2/3 可视区域并停留 1 秒后的曝光上报
22613
22646
  // 注意:不再主动调用 load(),让视频在自动播放时才开始加载
22614
22647
  console.log('[Carousel Video] 进入可视区域,索引:', index);
@@ -22942,8 +22975,8 @@ const StructurePage = (_a) => {
22942
22975
  }
22943
22976
  return textStyle;
22944
22977
  })()) }, (_s = data.carouselSection[carouselIndex]) === null || _s === void 0 ? void 0 : _s.text))),
22945
- renderCTA('carouselButton', (_v = (_u = (_t = data.carouselSection[carouselIndex]) === null || _t === void 0 ? void 0 : _t.bindProducts) === null || _u === void 0 ? void 0 : _u[0]) === null || _v === void 0 ? void 0 : _v.bindCta, (_x = (_w = data.carouselSection[carouselIndex]) === null || _w === void 0 ? void 0 : _w.bindProducts) === null || _x === void 0 ? void 0 : _x[0], baseStyles.carouselButton, 'carouselSection', carouselIndex)))),
22946
- data.highlightRevealSection && (React.createElement(ProductView, { onExposure: () => handleProductViewExposure(data.highlightRevealSection, 'highlightRevealSection', 0), onLeave: () => handleProductViewLeave(data.highlightRevealSection, 'highlightRevealSection', 0) },
22978
+ React.createElement(React.Fragment, { key: carouselIndex }, renderCTA('carouselButton', (_v = (_u = (_t = data.carouselSection[carouselIndex]) === null || _t === void 0 ? void 0 : _t.bindProducts) === null || _u === void 0 ? void 0 : _u[0]) === null || _v === void 0 ? void 0 : _v.bindCta, (_x = (_w = data.carouselSection[carouselIndex]) === null || _w === void 0 ? void 0 : _w.bindProducts) === null || _x === void 0 ? void 0 : _x[0], baseStyles.carouselButton, 'carouselSection', carouselIndex))))),
22979
+ data.highlightRevealSection && (React.createElement(ProductView, { onExposure: () => handleProductViewExposure(data.highlightRevealSection, 'highlightRevealSection', 0), onLeave: () => handleProductViewLeave(data.highlightRevealSection, 'highlightRevealSection', 0), pageHideCountRef: pageHideCountRef },
22947
22980
  React.createElement("div", { style: mergeStyles(baseStyles.highlightSection, 'highlightSection', { excludeBorder: true }) },
22948
22981
  React.createElement("div", { ref: highlightImageRef, style: Object.assign(Object.assign({}, baseStyles.highlightImageContainer), { position: 'relative' }) },
22949
22982
  shouldLoadHighlightImage && (React.createElement("img", { src: data.highlightRevealSection.landingImageUrl || data.highlightRevealSection.cover, alt: data.highlightRevealSection.title, style: Object.assign(Object.assign({}, baseStyles.highlightImage), { position: 'absolute', zIndex: 2 }) })),
@@ -23030,7 +23063,7 @@ const StructurePage = (_a) => {
23030
23063
  // 使用产品在数据数组中的实际索引来确定 buttonKey
23031
23064
  const productDataIndex = productIndexMap[gridIndex];
23032
23065
  const buttonKey = `productButton${productDataIndex || gridIndex + 1}`;
23033
- return (React.createElement("div", { key: (product === null || product === void 0 ? void 0 : product.itemId) || `empty-${gridIndex}`, style: baseStyles.productItem }, product ? (React.createElement(ProductView, { onExposure: () => handleProductViewExposure(product, 'productGridSection', 0), onLeave: () => handleProductViewLeave(product, 'productGridSection', 0) },
23066
+ return (React.createElement("div", { key: (product === null || product === void 0 ? void 0 : product.itemId) || `empty-${gridIndex}`, style: baseStyles.productItem }, product ? (React.createElement(ProductView, { onExposure: () => handleProductViewExposure(product, 'productGridSection', 0), onLeave: () => handleProductViewLeave(product, 'productGridSection', 0), pageHideCountRef: pageHideCountRef },
23034
23067
  React.createElement(React.Fragment, null,
23035
23068
  React.createElement("div", { ref: (el) => { productImageRefs.current[productIndexMap[gridIndex] - 1] = el; }, style: baseStyles.productImageContainer },
23036
23069
  shouldLoadProductImages[productIndexMap[gridIndex] - 1] && (React.createElement("img", { src: product.landingImageUrl || product.cover,
@@ -23042,7 +23075,7 @@ const StructurePage = (_a) => {
23042
23075
  React.createElement("div", { style: { width: '100%', height: 0, visibility: 'hidden' } }))));
23043
23076
  }).filter(Boolean);
23044
23077
  })())),
23045
- data.footerSection && (React.createElement(ProductView, { onExposure: () => handleProductViewExposure(data.footerSection, 'footerSection', 0), onLeave: () => handleProductViewLeave(data.footerSection, 'footerSection', 0) },
23078
+ data.footerSection && (React.createElement(ProductView, { onExposure: () => handleProductViewExposure(data.footerSection, 'footerSection', 0), onLeave: () => handleProductViewLeave(data.footerSection, 'footerSection', 0), pageHideCountRef: pageHideCountRef },
23046
23079
  React.createElement("div", { style: mergeStyles(baseStyles.footerSection, 'footerSection', { excludeBorder: true }) },
23047
23080
  React.createElement("div", { style: {
23048
23081
  width: '100%',