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 +136 -103
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +136 -103
- package/dist/index.js.map +1 -1
- package/dist/index.min.cjs +7 -7
- package/dist/index.min.cjs.map +1 -1
- package/dist/index.min.js +7 -7
- package/dist/index.min.js.map +1 -1
- package/dist/pb-ui.js +136 -103
- package/dist/pb-ui.js.map +1 -1
- package/dist/pb-ui.min.js +7 -7
- package/dist/pb-ui.min.js.map +1 -1
- package/es/core/components/StructurePage/index.js +56 -73
- package/es/core/components/SxpPageRender/typing.d.ts +4 -0
- package/es/core/context/SxpDataSourceProvider.d.ts +4 -0
- package/es/core/context/SxpDataSourceProvider.js +22 -7
- package/es/core/hooks/useAdvancedOnScreen.d.ts +2 -1
- package/es/core/hooks/useAdvancedOnScreen.js +5 -1
- package/es/materials/sxp/popup/AddToCart/index.js +25 -9
- package/lib/core/components/StructurePage/index.js +56 -73
- package/lib/core/components/SxpPageRender/typing.d.ts +4 -0
- package/lib/core/context/SxpDataSourceProvider.d.ts +4 -0
- package/lib/core/context/SxpDataSourceProvider.js +22 -7
- package/lib/core/hooks/useAdvancedOnScreen.d.ts +2 -1
- package/lib/core/hooks/useAdvancedOnScreen.js +5 -1
- package/lib/materials/sxp/popup/AddToCart/index.js +25 -9
- package/package.json +1 -1
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 = (
|
|
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 = (
|
|
1817
|
-
gldata = (
|
|
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 ((
|
|
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 = ((
|
|
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
|
|
12348
|
-
const shopifyDomain = (
|
|
12349
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
19620
|
-
|
|
19621
|
-
|
|
19622
|
-
|
|
19623
|
-
|
|
19624
|
-
|
|
19625
|
-
|
|
19626
|
-
|
|
19627
|
-
|
|
19628
|
-
|
|
19629
|
-
|
|
19630
|
-
|
|
19631
|
-
|
|
19632
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
20283
|
-
const clickCtaPosition = viewPosition === '
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
20533
|
+
// productGridSection 和 carouselSection 的 CTA 曝光事件 position 统一传 0
|
|
20496
20534
|
let reportPosition = 0;
|
|
20497
|
-
if (viewPosition
|
|
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
|
|
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
|
|
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%',
|