pb-sxp-ui 1.20.46 → 1.20.48

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
@@ -19648,18 +19648,23 @@ const PostView = ({ children, videoRef, isVideo, onExposure, autoPlayTriggerRef,
19648
19648
  autoPlayTriggerRef.current = true;
19649
19649
  console.log('[PostView onVisible] 已设置自动播放标记');
19650
19650
  }
19651
+ // 立即更新暂停图标状态,不等待 play() Promise
19652
+ // 这样可以确保图标状态与视频播放状态同步
19653
+ if (onVideoPlay) {
19654
+ onVideoPlay();
19655
+ }
19651
19656
  videoRef.current.play().then(() => {
19652
19657
  console.log('[PostView onVisible] 视频 play() 成功');
19653
- // 触发播放回调,更新暂停图标状态
19654
- if (onVideoPlay) {
19655
- onVideoPlay();
19656
- }
19657
19658
  }).catch(err => {
19658
19659
  console.warn('[PostView] Video autoplay failed:', err);
19659
- // 播放失败时重置标记
19660
+ // 播放失败时重置标记和图标状态
19660
19661
  if (autoPlayTriggerRef) {
19661
19662
  autoPlayTriggerRef.current = false;
19662
19663
  }
19664
+ // 播放失败,恢复暂停图标
19665
+ if (onVideoPause) {
19666
+ onVideoPause();
19667
+ }
19663
19668
  });
19664
19669
  }
19665
19670
  },
@@ -20011,7 +20016,7 @@ const baseStyles = {
20011
20016
  }
20012
20017
  };
20013
20018
  const StructurePage = (_a) => {
20014
- var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u;
20019
+ var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
20015
20020
  var { containerStyle, containerHeight = 664, containerWidth = 375, className = '', apiUrl = 'https://bff-be-dev.chatlabs.net/api/v1/recommend/list', requestBody, editorMode = false, multiCTAConfig: propMultiCTAConfig, videoPlayIcon: propVideoPlayIcon, isCmsMode = false, storyId, customHeaders } = _a, rest = __rest(_a, ["containerStyle", "containerHeight", "containerWidth", "className", "apiUrl", "requestBody", "editorMode", "multiCTAConfig", "videoPlayIcon", "isCmsMode", "storyId", "customHeaders"]);
20016
20021
  const [data, setData] = React.useState(null);
20017
20022
  const [loading, setLoading] = React.useState(true);
@@ -20024,24 +20029,28 @@ const StructurePage = (_a) => {
20024
20029
  // 视频暂停状态管理
20025
20030
  const [isHeroVideoPaused, setIsHeroVideoPaused] = React.useState(false);
20026
20031
  const [carouselVideoPausedStates, setCarouselVideoPausedStates] = React.useState([]);
20027
- // 引入事件上报 hooks
20028
- const { ctaEvent, bffFbReport, bffEventReport } = useSxpDataSource();
20032
+ // 引入事件上报 hooks 和 Session 刷新功能
20033
+ const { ctaEvent, bffFbReport, bffEventReport, refreshFeSession, curTime } = useSxpDataSource();
20029
20034
  // 防止重复上报的标记
20030
20035
  const h5EnterReportedRef = React.useRef(false);
20031
- // CTA 曝光状态追踪(避免重复上报)
20032
- const exposedCtaRefs = React.useRef(new Set());
20036
+ // 全局事件管理:用于追踪弹窗打开时间和状态
20037
+ const popupCurTimeRef = React.useRef(new Date());
20038
+ const currentPopupDataRef = React.useRef(null);
20039
+ // CTA 曝光时间追踪(允许重复上报)
20040
+ const ctaExposureTime = React.useRef({});
20033
20041
  // Carousel 图片浏览时间追踪
20034
20042
  const carouselImageStartTime = React.useRef(0);
20035
20043
  const carouselImageLoadTime = React.useRef({});
20036
- // 产品浏览状态追踪(避免重复上报)
20037
- const exposedProductRefs = React.useRef(new Set());
20044
+ // 产品浏览状态追踪(记录每次浏览的开始时间)
20045
+ // 注意:不使用 Set 来避免重复,因为用户每次浏览都应该上报
20038
20046
  const productViewStartTime = React.useRef({});
20039
20047
  // 视频播放状态追踪
20040
20048
  const videoStartTime = React.useRef({});
20041
20049
  const isFirstPlay = React.useRef({});
20042
20050
  const videoLoadTime = React.useRef({});
20043
- const videoAutoPlayReported = React.useRef(new Set()); // 追踪哪些视频已通过自动播放上报
20051
+ // 注释:移除 videoAutoPlayReported,允许视频重复上报
20044
20052
  const videoPlayReportTime = React.useRef({}); // 追踪视频播放上报的时间戳,防止短时间内重复上报
20053
+ const videoLastCurrentTime = React.useRef({}); // 追踪视频的上一次播放时间,用于检测循环
20045
20054
  // 自动播放触发标记(用于区分自动播放和用户手动播放)
20046
20055
  const heroAutoPlayTrigger = React.useRef(false);
20047
20056
  const carouselAutoPlayTriggers = React.useRef({});
@@ -20061,6 +20070,11 @@ const StructurePage = (_a) => {
20061
20070
  var _a;
20062
20071
  return propMultiCTAConfig || ((_a = schema === null || schema === void 0 ? void 0 : schema.sxpPageConf) === null || _a === void 0 ? void 0 : _a.multiCTAConfig) || {};
20063
20072
  }, [propMultiCTAConfig, (_b = schema === null || schema === void 0 ? void 0 : schema.sxpPageConf) === null || _b === void 0 ? void 0 : _b.multiCTAConfig]);
20073
+ // 获取 globalConfig 中的 session 配置
20074
+ const globalConfig = React.useMemo(() => {
20075
+ var _a;
20076
+ return (_a = schema === null || schema === void 0 ? void 0 : schema.sxpPageConf) === null || _a === void 0 ? void 0 : _a.globalConfig;
20077
+ }, [(_c = schema === null || schema === void 0 ? void 0 : schema.sxpPageConf) === null || _c === void 0 ? void 0 : _c.globalConfig]);
20064
20078
  // 默认播放图标 URL(直接使用 CDN 地址作为后备)
20065
20079
  const DEFAULT_PAUSE_ICON = 'https://sxph5-uat.chatlabs.net/pb_static/06f28a2025c74c1cb49be6767316d827.png';
20066
20080
  // 获取视频播放图标
@@ -20084,7 +20098,7 @@ const StructurePage = (_a) => {
20084
20098
  }
20085
20099
  // 最后使用默认图标
20086
20100
  return DEFAULT_PAUSE_ICON;
20087
- }, [propVideoPlayIcon, (_d = (_c = schema === null || schema === void 0 ? void 0 : schema.sxpPageConf) === null || _c === void 0 ? void 0 : _c.globalConfig) === null || _d === void 0 ? void 0 : _d.videoPlayIcon]);
20101
+ }, [propVideoPlayIcon, (_e = (_d = schema === null || schema === void 0 ? void 0 : schema.sxpPageConf) === null || _d === void 0 ? void 0 : _d.globalConfig) === null || _e === void 0 ? void 0 : _e.videoPlayIcon]);
20088
20102
  // 处理 CTA 点击
20089
20103
  const handleCtaClick = React.useCallback((link, interaction, productData, ctaData, viewPosition, sectionIndex) => {
20090
20104
  // 判断是否为外部链接
@@ -20135,6 +20149,46 @@ const StructurePage = (_a) => {
20135
20149
  if (interaction) {
20136
20150
  const { linkType, popupType, popupAni } = interaction;
20137
20151
  if (linkType === 'popup' && popupType) {
20152
+ // ========== 打开弹窗前:记录弹窗数据和时间 ==========
20153
+ // 重置弹窗时间
20154
+ popupCurTimeRef.current = new Date();
20155
+ // 保存当前弹窗的产品数据,用于全局事件监听
20156
+ currentPopupDataRef.current = {
20157
+ productData,
20158
+ ctaData,
20159
+ viewPosition,
20160
+ sectionIndex,
20161
+ popupType
20162
+ };
20163
+ console.log('[StructurePage] 打开弹窗,记录数据:', {
20164
+ productId: productData === null || productData === void 0 ? void 0 : productData.itemId,
20165
+ viewPosition,
20166
+ sectionIndex,
20167
+ popupType
20168
+ });
20169
+ // ========== 弹窗打开时:立即上报 ProductView 和 PageView 事件 ==========
20170
+ // 这与非 multi-cta 的 CommodityDetail 组件行为一致
20171
+ const recData = Object.assign(Object.assign({}, rec), { video: (rec === null || rec === void 0 ? void 0 : rec.video) ? Object.assign(Object.assign({}, rec.video), { title: '', imgUrls: (productData === null || productData === void 0 ? void 0 : productData.homePage) || rec.video.imgUrls, url: rec.video.url || null }) : null });
20172
+ // 上报 ProductView 事件
20173
+ bffFbReport === null || bffFbReport === void 0 ? void 0 : bffFbReport({
20174
+ eventName: 'ProductView',
20175
+ product: productData ? [productData] : undefined,
20176
+ contentType: 'product',
20177
+ rec: recData,
20178
+ position: sectionIndex !== undefined ? sectionIndex : 0
20179
+ });
20180
+ // 上报 PageView 事件
20181
+ bffFbReport === null || bffFbReport === void 0 ? void 0 : bffFbReport({
20182
+ eventName: 'PageView',
20183
+ product: productData ? [productData] : undefined,
20184
+ contentType: 'product',
20185
+ rec: recData,
20186
+ position: sectionIndex !== undefined ? sectionIndex : 0
20187
+ });
20188
+ console.log('[StructurePage] 上报弹窗打开事件 (ProductView + PageView)', {
20189
+ productId: productData === null || productData === void 0 ? void 0 : productData.itemId,
20190
+ position: sectionIndex
20191
+ });
20138
20192
  // 设置弹窗要显示的产品数据
20139
20193
  if (productData && typeof window !== 'undefined' && window.setPopupDetailData) {
20140
20194
  // 构造与原有系统一致的数据结构
@@ -20243,16 +20297,19 @@ const StructurePage = (_a) => {
20243
20297
  const finalStyle = Object.assign(Object.assign({}, baseStyle), customStyle);
20244
20298
  return finalStyle;
20245
20299
  }, [multiCTAConfig]);
20246
- // CTA 曝光事件处理
20300
+ // CTA 曝光事件处理(允许重复上报)
20247
20301
  const handleCtaExposure = React.useCallback((ctaData, productData, viewPosition, sectionIndex, buttonKey) => {
20248
- // 生成唯一标识,避免重复上报
20302
+ // 生成唯一标识
20249
20303
  const ctaId = `${buttonKey}-${viewPosition}-${sectionIndex}`;
20250
- // 如果已经上报过,不再上报
20251
- if (exposedCtaRefs.current.has(ctaId)) {
20304
+ // 记录本次曝光时间(允许重复上报)
20305
+ const now = Date.now();
20306
+ const lastExposureTime = ctaExposureTime.current[ctaId];
20307
+ // 防抖:如果距离上次曝光不到 1 秒,跳过(避免同一次曝光重复触发)
20308
+ if (lastExposureTime && (now - lastExposureTime) < 1000) {
20252
20309
  return;
20253
20310
  }
20254
- // 标记为已上报
20255
- exposedCtaRefs.current.add(ctaId);
20311
+ // 更新曝光时间
20312
+ ctaExposureTime.current[ctaId] = now;
20256
20313
  // 获取当前 section 的 post/image 数据(用于 contentId 和 sceneId)
20257
20314
  let postData = null;
20258
20315
  if (viewPosition === 'heroSection' && (data === null || data === void 0 ? void 0 : data.heroSection)) {
@@ -20313,20 +20370,8 @@ const StructurePage = (_a) => {
20313
20370
  carouselAutoPlayTriggers.current[sectionIndex] = false; // 重置标记
20314
20371
  }
20315
20372
  console.log('[handleVideoPlay] isAutoPlay:', isAutoPlay, 'videoId:', videoId);
20316
- // 如果是自动播放,标记为已上报
20317
- if (isAutoPlay) {
20318
- videoAutoPlayReported.current.add(videoId);
20319
- console.log('[handleVideoPlay] 标记为已通过自动播放上报');
20320
- }
20321
- else {
20322
- // 如果不是自动播放,但已经通过自动播放上报过,则跳过
20323
- if (videoAutoPlayReported.current.has(videoId)) {
20324
- console.log('[handleVideoPlay] 跳过上报:已经通过自动播放上报过');
20325
- // 只记录播放时间,不上报
20326
- videoStartTime.current[videoId] = videoRef.currentTime || 0;
20327
- return;
20328
- }
20329
- }
20373
+ // 记录播放上报(允许重复上报,已通过 videoPlayReportTime 防抖)
20374
+ console.log('[handleVideoPlay] 视频播放上报,isAutoPlay:', isAutoPlay);
20330
20375
  // 记录视频开始播放时间
20331
20376
  videoStartTime.current[videoId] = videoRef.currentTime || 0;
20332
20377
  // 记录本次上报时间
@@ -20352,8 +20397,48 @@ const StructurePage = (_a) => {
20352
20397
  }))
20353
20398
  });
20354
20399
  }, [bffEventReport]);
20400
+ // 视频时间更新事件处理(用于检测循环播放)
20401
+ const handleVideoTimeUpdate = React.useCallback((videoData, viewPosition, sectionIndex, videoRef) => {
20402
+ if (!videoRef || !videoRef.duration)
20403
+ return;
20404
+ const videoId = `${videoData === null || videoData === void 0 ? void 0 : videoData.itemId}-${viewPosition}-${sectionIndex}`;
20405
+ const currentTime = videoRef.currentTime;
20406
+ const lastTime = videoLastCurrentTime.current[videoId] || 0;
20407
+ // 检测视频循环(当前时间明显小于上次时间,说明视频重新开始了)
20408
+ if (lastTime > 0 && currentTime < lastTime && lastTime > videoRef.duration * 0.9) {
20409
+ console.log('[handleVideoTimeUpdate] 检测到视频循环,currentTime:', currentTime, 'lastTime:', lastTime);
20410
+ // 上报 playOverVideo(循环结束)
20411
+ const videoDuration = videoRef.duration.toFixed(2);
20412
+ const startTime = videoStartTime.current[videoId] || 0;
20413
+ const playDuration = (lastTime - startTime).toFixed(2);
20414
+ if (parseFloat(playDuration) >= 0.1) {
20415
+ console.log('[handleVideoTimeUpdate] 上报循环结束的 playOverVideo 事件');
20416
+ bffEventReport === null || bffEventReport === void 0 ? void 0 : bffEventReport({
20417
+ eventInfo: {
20418
+ eventSubject: 'playOverVideo',
20419
+ eventDescription: 'User finished playing the video',
20420
+ sceneId: '',
20421
+ contentId: (videoData === null || videoData === void 0 ? void 0 : videoData.itemId) || '',
20422
+ contentName: (videoData === null || videoData === void 0 ? void 0 : videoData.title) || '',
20423
+ endTime: videoDuration,
20424
+ videoDuration,
20425
+ playDuration,
20426
+ contentTags: (videoData === null || videoData === void 0 ? void 0 : videoData.tags) ? JSON.stringify(videoData.tags) : '[]',
20427
+ position: `${sectionIndex || 0}`,
20428
+ contentFormat: 'video',
20429
+ rtc: '',
20430
+ traceInfo: (videoData === null || videoData === void 0 ? void 0 : videoData.traceInfo) || ''
20431
+ }
20432
+ });
20433
+ }
20434
+ // 重置上次上报时间,让下次 play 事件可以上报
20435
+ delete videoPlayReportTime.current[videoId];
20436
+ }
20437
+ // 更新上次播放时间
20438
+ videoLastCurrentTime.current[videoId] = currentTime;
20439
+ }, [bffEventReport]);
20355
20440
  // 视频播放结束/暂停事件处理
20356
- const handleVideoPlayOver = React.useCallback((videoData, viewPosition, sectionIndex, videoRef) => {
20441
+ const handleVideoPlayOver = React.useCallback((videoData, viewPosition, sectionIndex, videoRef, isEndedEvent) => {
20357
20442
  if (!videoRef || !videoRef.duration)
20358
20443
  return;
20359
20444
  const videoId = `${videoData === null || videoData === void 0 ? void 0 : videoData.itemId}-${viewPosition}-${sectionIndex}`;
@@ -20362,6 +20447,18 @@ const StructurePage = (_a) => {
20362
20447
  // 计算播放时长
20363
20448
  const startTime = videoStartTime.current[videoId] || 0;
20364
20449
  const playDuration = (videoRef.currentTime - startTime).toFixed(2);
20450
+ // 如果播放时长过短(小于100ms),可能是误触发,不上报
20451
+ if (parseFloat(playDuration) < 0.1) {
20452
+ console.log('[handleVideoPlayOver] 播放时长过短,跳过上报:', playDuration);
20453
+ return;
20454
+ }
20455
+ console.log('[handleVideoPlayOver] 上报 playOverVideo 事件:', {
20456
+ videoId,
20457
+ isEndedEvent,
20458
+ endTime,
20459
+ playDuration,
20460
+ videoDuration
20461
+ });
20365
20462
  bffEventReport === null || bffEventReport === void 0 ? void 0 : bffEventReport({
20366
20463
  eventInfo: {
20367
20464
  eventSubject: 'playOverVideo',
@@ -20406,11 +20503,7 @@ const StructurePage = (_a) => {
20406
20503
  const handleProductViewExposure = React.useCallback((productData, viewPosition, sectionIndex) => {
20407
20504
  // 生成唯一标识
20408
20505
  const productId = `${productData === null || productData === void 0 ? void 0 : productData.itemId}-${viewPosition}-${sectionIndex}`;
20409
- // 如果已经记录过开始时间,不再重复记录
20410
- if (productViewStartTime.current[productId]) {
20411
- return;
20412
- }
20413
- // 记录开始浏览时间
20506
+ // 每次进入可视区域都记录新的开始时间(允许重复浏览)
20414
20507
  productViewStartTime.current[productId] = Date.now();
20415
20508
  console.log('[handleProductViewExposure] 记录产品开始浏览时间:', {
20416
20509
  productId,
@@ -20422,10 +20515,6 @@ const StructurePage = (_a) => {
20422
20515
  var _a;
20423
20516
  // 生成唯一标识
20424
20517
  const productId = `${productData === null || productData === void 0 ? void 0 : productData.itemId}-${viewPosition}-${sectionIndex}`;
20425
- // 如果已经上报过,不再重复上报
20426
- if (exposedProductRefs.current.has(productId)) {
20427
- return;
20428
- }
20429
20518
  // 获取开始浏览时间
20430
20519
  const startTime = productViewStartTime.current[productId];
20431
20520
  if (!startTime) {
@@ -20434,8 +20523,8 @@ const StructurePage = (_a) => {
20434
20523
  }
20435
20524
  // 计算停留时长(秒)
20436
20525
  const timeOnSite = Math.floor((Date.now() - startTime) / 1000);
20437
- // 标记为已上报
20438
- exposedProductRefs.current.add(productId);
20526
+ // 清除开始时间记录,以便下次进入时能再次记录
20527
+ delete productViewStartTime.current[productId];
20439
20528
  // 根据 viewPosition 确定 fromKName
20440
20529
  let fromKName = 'productPage';
20441
20530
  if (viewPosition === 'heroSection') {
@@ -20733,16 +20822,20 @@ const StructurePage = (_a) => {
20733
20822
  }
20734
20823
  }
20735
20824
  }, []);
20736
- // Post 图片/视频曝光上报(用于 Hero Section 等非轮播的 Post)
20737
- const postExposureReported = React.useRef(new Set());
20825
+ // Post 图片/视频曝光时间追踪(允许重复上报)
20826
+ const postExposureTime = React.useRef({});
20738
20827
  React.useCallback((postData, viewPosition, sectionIndex) => {
20739
20828
  var _a;
20740
20829
  const postId = `${postData === null || postData === void 0 ? void 0 : postData.itemId}-${viewPosition}-${sectionIndex}`;
20741
- // 防止重复上报
20742
- if (postExposureReported.current.has(postId)) {
20830
+ // 记录本次曝光时间(允许重复上报)
20831
+ const now = Date.now();
20832
+ const lastExposureTime = postExposureTime.current[postId];
20833
+ // 防抖:如果距离上次曝光不到 1 秒,跳过(避免同一次曝光重复触发)
20834
+ if (lastExposureTime && (now - lastExposureTime) < 1000) {
20743
20835
  return;
20744
20836
  }
20745
- postExposureReported.current.add(postId);
20837
+ // 更新曝光时间
20838
+ postExposureTime.current[postId] = now;
20746
20839
  // 判断是图片还是视频
20747
20840
  const contentFormat = (postData === null || postData === void 0 ? void 0 : postData.url) ? 'video' : 'image';
20748
20841
  const eventSubject = contentFormat === 'video' ? 'viewVideo' : 'viewImage';
@@ -20959,6 +21052,270 @@ const StructurePage = (_a) => {
20959
21052
  setTouchStart(null);
20960
21053
  setTouchEnd(null);
20961
21054
  };
21055
+ // ==================== 全局事件监听:visibilitychange ====================
21056
+ // 监听页面可见性变化(最小化、切换标签页、返回等)
21057
+ // 参考 SxpPageRender 的实现
21058
+ React.useEffect(() => {
21059
+ const handleVisibilityChange = () => {
21060
+ var _a, _b;
21061
+ if (document.visibilityState === 'hidden') {
21062
+ // ========== 页面隐藏时 ==========
21063
+ console.log('[StructurePage] Page hidden - 触发全局事件');
21064
+ // 1. 发送 PAGE_DID_HIDE 事件到事件总线
21065
+ SXP_EVENT_BUS.emit(SXP_EVENT_TYPE.PAGE_DID_HIDE, data);
21066
+ // 2. 如果有打开的弹窗,上报 ExitFeed 事件
21067
+ if (currentPopupDataRef.current) {
21068
+ const { productData, viewPosition, sectionIndex } = currentPopupDataRef.current;
21069
+ // 获取当前 section 的 post 数据
21070
+ let postData = null;
21071
+ if (viewPosition === 'heroSection' && (data === null || data === void 0 ? void 0 : data.heroSection)) {
21072
+ postData = data.heroSection;
21073
+ }
21074
+ else if (viewPosition === 'carouselSection' && (data === null || data === void 0 ? void 0 : data.carouselSection) && sectionIndex !== undefined) {
21075
+ postData = data.carouselSection[sectionIndex];
21076
+ }
21077
+ const rec = {
21078
+ video: {
21079
+ bindProduct: productData,
21080
+ bindProducts: productData ? [productData] : [],
21081
+ itemId: (postData === null || postData === void 0 ? void 0 : postData.itemId) || '',
21082
+ tags: (postData === null || postData === void 0 ? void 0 : postData.tags) || null,
21083
+ scene: (postData === null || postData === void 0 ? void 0 : postData.scene) || null,
21084
+ url: (postData === null || postData === void 0 ? void 0 : postData.url) || null,
21085
+ imgUrls: (postData === null || postData === void 0 ? void 0 : postData.imgUrls) || null
21086
+ },
21087
+ product: null
21088
+ };
21089
+ const viewTime = new Date() - popupCurTimeRef.current;
21090
+ bffFbReport === null || bffFbReport === void 0 ? void 0 : bffFbReport({
21091
+ eventName: 'ExitFeed',
21092
+ product: productData ? [productData] : undefined,
21093
+ contentType: 'product',
21094
+ rec: rec,
21095
+ position: sectionIndex !== undefined ? sectionIndex : 0,
21096
+ view_time: viewTime,
21097
+ content_id: (productData === null || productData === void 0 ? void 0 : productData.itemId) || ''
21098
+ });
21099
+ console.log('[StructurePage] 上报 ExitFeed 事件', { viewTime, productId: productData === null || productData === void 0 ? void 0 : productData.itemId });
21100
+ }
21101
+ // 3. 刷新 Session(处理 Session 过期)
21102
+ // 只有在非 organic menu 模式下才刷新 Session(与非 multi-cta 逻辑一致)
21103
+ if ((globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.playbook) !== 'organic menu' && refreshFeSession && curTime) {
21104
+ const isExpire = refreshFeSession(false, (reason) => {
21105
+ console.log('[StructurePage] Session expired:', reason);
21106
+ // Session 过期时的回调处理
21107
+ bffEventReport === null || bffEventReport === void 0 ? void 0 : bffEventReport({
21108
+ eventInfo: {
21109
+ eventSubject: 'sessionExpired',
21110
+ eventDescription: 'User session has expired',
21111
+ reason
21112
+ }
21113
+ });
21114
+ });
21115
+ // 只有当明确返回 false 时才上报 sessionCompleted
21116
+ if (typeof isExpire !== 'undefined' && !isExpire) {
21117
+ // Session 未过期,上报正常的 Session 完成事件
21118
+ const sessionDuration = (new Date() - curTime.current) / 1000;
21119
+ bffEventReport === null || bffEventReport === void 0 ? void 0 : bffEventReport({
21120
+ eventInfo: {
21121
+ eventSubject: 'sessionCompleted',
21122
+ eventDescription: 'User session completed normally',
21123
+ duration: `${sessionDuration}`
21124
+ }
21125
+ });
21126
+ }
21127
+ }
21128
+ }
21129
+ else if (document.visibilityState === 'visible') {
21130
+ // ========== 页面可见时 ==========
21131
+ console.log('[StructurePage] Page visible - 触发全局事件');
21132
+ // ⚠️ 重要:必须先刷新 Session,再上报事件(使用最新的 Session ID)
21133
+ // 1. 刷新 Session(根据配置决定)
21134
+ if (refreshFeSession) {
21135
+ refreshFeSession(false);
21136
+ }
21137
+ // 2. 如果未启用 session 配置,则强制刷新 Session ID(与非 multi-cta 逻辑一致)
21138
+ // ⚠️ 必须在上报事件之前刷新,这样上报的事件才会使用最新的 Session ID
21139
+ if (!((_a = globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.session) === null || _a === void 0 ? void 0 : _a.enable)) {
21140
+ refreshFeSessionId();
21141
+ console.log('[StructurePage] Session 未启用,强制刷新 Session ID');
21142
+ }
21143
+ // 3. 上报 h5LinkEnterFeed 事件(现在使用的是刷新后的 Session ID)
21144
+ const getUrlParamByKey = (key) => {
21145
+ const urlParams = new URLSearchParams(window.location.search);
21146
+ return urlParams.get(key) || '';
21147
+ };
21148
+ const time = new Date();
21149
+ if (curTime) {
21150
+ curTime.current = time;
21151
+ }
21152
+ bffEventReport === null || bffEventReport === void 0 ? void 0 : bffEventReport({
21153
+ eventInfo: {
21154
+ eventSubject: 'h5LinkEnterFeed',
21155
+ eventDescription: 'User enter h5 link',
21156
+ utmSource: getUrlParamByKey('utm_source'),
21157
+ utmMedium: getUrlParamByKey('utm_medium'),
21158
+ utmCampaign: getUrlParamByKey('utm_campaign'),
21159
+ utmId: getUrlParamByKey('utm_id'),
21160
+ utmContent: getUrlParamByKey('utm_content'),
21161
+ enterTime: Math.floor(time / 1000) + '',
21162
+ requestId: '',
21163
+ enterUrl: ((_b = window === null || window === void 0 ? void 0 : window.location) === null || _b === void 0 ? void 0 : _b.href) || ''
21164
+ },
21165
+ reportLayId: false
21166
+ });
21167
+ // 3. 发送 PAGE_DID_SHOW 事件到事件总线(延迟 100ms,与 SxpPageRender 保持一致)
21168
+ setTimeout(() => {
21169
+ SXP_EVENT_BUS.emit(SXP_EVENT_TYPE.PAGE_DID_SHOW, data);
21170
+ }, 100);
21171
+ // 4. 如果有打开的弹窗,重置弹窗时间
21172
+ if (currentPopupDataRef.current) {
21173
+ popupCurTimeRef.current = new Date();
21174
+ console.log('[StructurePage] 重置弹窗计时器');
21175
+ }
21176
+ }
21177
+ };
21178
+ // 添加事件监听
21179
+ document.addEventListener('visibilitychange', handleVisibilityChange);
21180
+ // 清理函数
21181
+ return () => {
21182
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
21183
+ };
21184
+ }, [data, bffFbReport, bffEventReport, refreshFeSession, curTime, globalConfig]);
21185
+ // ==================== 初始化弹窗时间记录 ====================
21186
+ React.useEffect(() => {
21187
+ const initTime = () => {
21188
+ popupCurTimeRef.current = new Date();
21189
+ };
21190
+ initTime();
21191
+ window.addEventListener('pageshow', initTime);
21192
+ return () => {
21193
+ window.removeEventListener('pageshow', initTime);
21194
+ };
21195
+ }, []);
21196
+ // ==================== 监听弹窗关闭事件 ====================
21197
+ // 通过轮询检测弹窗是否关闭(因为弹窗关闭时会清空 popupDetailData)
21198
+ React.useEffect(() => {
21199
+ let checkInterval = null;
21200
+ let wasPopupOpen = false;
21201
+ const checkPopupStatus = () => {
21202
+ // 检查是否有弹窗打开
21203
+ const isPopupOpen = typeof window !== 'undefined' &&
21204
+ window.popup &&
21205
+ window.popup.id !== '';
21206
+ // 检测从打开到关闭的转换
21207
+ if (wasPopupOpen && !isPopupOpen && currentPopupDataRef.current) {
21208
+ console.log('[StructurePage] 检测到弹窗关闭');
21209
+ // 弹窗关闭时上报事件
21210
+ const { productData, viewPosition, sectionIndex } = currentPopupDataRef.current;
21211
+ // 获取当前 section 的 post 数据
21212
+ let postData = null;
21213
+ if (viewPosition === 'heroSection' && (data === null || data === void 0 ? void 0 : data.heroSection)) {
21214
+ postData = data.heroSection;
21215
+ }
21216
+ else if (viewPosition === 'carouselSection' && (data === null || data === void 0 ? void 0 : data.carouselSection) && sectionIndex !== undefined) {
21217
+ postData = data.carouselSection[sectionIndex];
21218
+ }
21219
+ const rec = {
21220
+ video: {
21221
+ bindProduct: productData,
21222
+ bindProducts: productData ? [productData] : [],
21223
+ itemId: (postData === null || postData === void 0 ? void 0 : postData.itemId) || '',
21224
+ tags: (postData === null || postData === void 0 ? void 0 : postData.tags) || null,
21225
+ scene: (postData === null || postData === void 0 ? void 0 : postData.scene) || null,
21226
+ url: (postData === null || postData === void 0 ? void 0 : postData.url) || null,
21227
+ imgUrls: (postData === null || postData === void 0 ? void 0 : postData.imgUrls) || null
21228
+ },
21229
+ product: null
21230
+ };
21231
+ const viewTime = new Date() - popupCurTimeRef.current;
21232
+ // 上报 Engagement 事件(engagement_type: 'close_popup')
21233
+ bffFbReport === null || bffFbReport === void 0 ? void 0 : bffFbReport({
21234
+ eventName: 'Engagement',
21235
+ product: productData ? [productData] : undefined,
21236
+ rec: rec,
21237
+ position: sectionIndex !== undefined ? sectionIndex : 0,
21238
+ content_id: (productData === null || productData === void 0 ? void 0 : productData.itemId) || '',
21239
+ engagement_type: 'close_popup',
21240
+ contentType: 'product',
21241
+ view_time: viewTime
21242
+ });
21243
+ console.log('[StructurePage] 上报弹窗关闭事件 (Engagement)', {
21244
+ viewTime,
21245
+ productId: productData === null || productData === void 0 ? void 0 : productData.itemId,
21246
+ engagement_type: 'close_popup'
21247
+ });
21248
+ // 清空弹窗数据引用
21249
+ currentPopupDataRef.current = null;
21250
+ }
21251
+ wasPopupOpen = isPopupOpen;
21252
+ };
21253
+ // 每 200ms 检查一次弹窗状态
21254
+ checkInterval = setInterval(checkPopupStatus, 200);
21255
+ return () => {
21256
+ if (checkInterval) {
21257
+ clearInterval(checkInterval);
21258
+ }
21259
+ };
21260
+ }, [data, bffFbReport]);
21261
+ // ==================== 监听 SXP_EVENT_BUS 事件(用于弹窗内的事件响应)====================
21262
+ // 当页面可见性变化时,如果有弹窗打开,需要相应处理
21263
+ React.useEffect(() => {
21264
+ // 弹窗打开时监听 PAGE_DID_SHOW 事件,重置计时器
21265
+ const onShow = () => {
21266
+ if (currentPopupDataRef.current) {
21267
+ popupCurTimeRef.current = new Date();
21268
+ console.log('[StructurePage] PAGE_DID_SHOW - 重置弹窗计时器');
21269
+ }
21270
+ };
21271
+ // 弹窗打开时监听 PAGE_DID_HIDE 事件,上报 ExitFeed
21272
+ const onHide = () => {
21273
+ if (currentPopupDataRef.current) {
21274
+ const { productData, viewPosition, sectionIndex } = currentPopupDataRef.current;
21275
+ // 获取当前 section 的 post 数据
21276
+ let postData = null;
21277
+ if (viewPosition === 'heroSection' && (data === null || data === void 0 ? void 0 : data.heroSection)) {
21278
+ postData = data.heroSection;
21279
+ }
21280
+ else if (viewPosition === 'carouselSection' && (data === null || data === void 0 ? void 0 : data.carouselSection) && sectionIndex !== undefined) {
21281
+ postData = data.carouselSection[sectionIndex];
21282
+ }
21283
+ const rec = {
21284
+ video: {
21285
+ bindProduct: productData,
21286
+ bindProducts: productData ? [productData] : [],
21287
+ itemId: (postData === null || postData === void 0 ? void 0 : postData.itemId) || '',
21288
+ tags: (postData === null || postData === void 0 ? void 0 : postData.tags) || null,
21289
+ scene: (postData === null || postData === void 0 ? void 0 : postData.scene) || null,
21290
+ url: (postData === null || postData === void 0 ? void 0 : postData.url) || null,
21291
+ imgUrls: (postData === null || postData === void 0 ? void 0 : postData.imgUrls) || null
21292
+ },
21293
+ product: null
21294
+ };
21295
+ const viewTime = new Date() - popupCurTimeRef.current;
21296
+ // 上报 ExitFeed 事件(页面隐藏时)
21297
+ bffFbReport === null || bffFbReport === void 0 ? void 0 : bffFbReport({
21298
+ eventName: 'ExitFeed',
21299
+ product: productData ? [productData] : undefined,
21300
+ contentType: 'product',
21301
+ rec: rec,
21302
+ position: sectionIndex !== undefined ? sectionIndex : 0,
21303
+ view_time: viewTime,
21304
+ content_id: (productData === null || productData === void 0 ? void 0 : productData.itemId) || ''
21305
+ });
21306
+ console.log('[StructurePage] PAGE_DID_HIDE - 上报 ExitFeed (弹窗内)', {
21307
+ viewTime,
21308
+ productId: productData === null || productData === void 0 ? void 0 : productData.itemId
21309
+ });
21310
+ }
21311
+ };
21312
+ SXP_EVENT_BUS.on(SXP_EVENT_TYPE.PAGE_DID_SHOW, onShow);
21313
+ SXP_EVENT_BUS.on(SXP_EVENT_TYPE.PAGE_DID_HIDE, onHide);
21314
+ return () => {
21315
+ SXP_EVENT_BUS.off(SXP_EVENT_TYPE.PAGE_DID_SHOW, onShow);
21316
+ SXP_EVENT_BUS.off(SXP_EVENT_TYPE.PAGE_DID_HIDE, onHide);
21317
+ };
21318
+ }, [data, bffFbReport]);
20962
21319
  if (loading) {
20963
21320
  return (React.createElement("div", { style: Object.assign(Object.assign({}, baseStyles.container), { height: containerHeight, width: containerWidth, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: '16px' }) }, "Loading..."));
20964
21321
  }
@@ -20990,7 +21347,7 @@ const StructurePage = (_a) => {
20990
21347
  return '20px';
20991
21348
  })(),
20992
21349
  backgroundColor: '#000',
20993
- textAlign: ((_e = multiCTAConfig === null || multiCTAConfig === void 0 ? void 0 : multiCTAConfig.heroTopText) === null || _e === void 0 ? void 0 : _e.textAlign) || 'center',
21350
+ textAlign: ((_f = multiCTAConfig === null || multiCTAConfig === void 0 ? void 0 : multiCTAConfig.heroTopText) === null || _f === void 0 ? void 0 : _f.textAlign) || 'center',
20994
21351
  boxSizing: 'border-box',
20995
21352
  display: 'flex',
20996
21353
  alignItems: 'center',
@@ -21049,7 +21406,15 @@ const StructurePage = (_a) => {
21049
21406
  return textStyle;
21050
21407
  })()) }, data.heroSection.text))),
21051
21408
  React.createElement("div", { style: baseStyles.heroImageContainer },
21052
- React.createElement(PostView, { videoRef: heroVideoRef, isVideo: !!data.heroSection.url, autoPlayTriggerRef: heroAutoPlayTrigger }, data.heroSection.url ? (React.createElement("div", { style: { position: 'relative', width: '100%', height: '100%' }, onClick: handleHeroVideoClick },
21409
+ React.createElement(PostView, { videoRef: heroVideoRef, isVideo: !!data.heroSection.url, autoPlayTriggerRef: heroAutoPlayTrigger, onVideoPlay: () => {
21410
+ // 视频开始播放,隐藏暂停图标
21411
+ console.log('[Hero PostView] onVideoPlay 回调');
21412
+ setIsHeroVideoPaused(false);
21413
+ }, onVideoPause: () => {
21414
+ // 视频暂停,显示暂停图标
21415
+ console.log('[Hero PostView] onVideoPause 回调');
21416
+ setIsHeroVideoPaused(true);
21417
+ } }, data.heroSection.url ? (React.createElement("div", { style: { position: 'relative', width: '100%', height: '100%' }, onClick: handleHeroVideoClick },
21053
21418
  React.createElement("video", { ref: (el) => {
21054
21419
  if (heroVideoRef) {
21055
21420
  heroVideoRef.current = el;
@@ -21075,7 +21440,10 @@ const StructurePage = (_a) => {
21075
21440
  });
21076
21441
  el.addEventListener('ended', (e) => {
21077
21442
  console.log('[Hero Video] ended event triggered');
21078
- handleVideoPlayOver(data.heroSection, 'heroSection', 0, e.currentTarget);
21443
+ handleVideoPlayOver(data.heroSection, 'heroSection', 0, e.currentTarget, true);
21444
+ });
21445
+ el.addEventListener('timeupdate', (e) => {
21446
+ handleVideoTimeUpdate(data.heroSection, 'heroSection', 0, e.currentTarget);
21079
21447
  });
21080
21448
  }
21081
21449
  }, src: data.heroSection.url, style: baseStyles.heroVideo, muted: true, loop: true, playsInline: true, controls: false, onLoadedMetadata: () => {
@@ -21085,8 +21453,8 @@ const StructurePage = (_a) => {
21085
21453
  const initTime = videoLoadTime.current[videoId] || loadTime;
21086
21454
  videoLoadTime.current[videoId] = loadTime - initTime;
21087
21455
  } }),
21088
- isHeroVideoPaused && (React.createElement(FormatImage$1, { className: 'clc-pb-video-pause', src: videoPlayIcon, alt: 'play' })))) : ((_f = data.heroSection.imgUrls) === null || _f === void 0 ? void 0 : _f[0]) ? (React.createElement("img", { src: data.heroSection.imgUrls[0], alt: 'Hero', style: baseStyles.heroImage })) : null),
21089
- React.createElement("div", { style: baseStyles.heroOverlay }, renderCTA('heroButton', (_h = (_g = data.heroSection.bindProducts) === null || _g === void 0 ? void 0 : _g[0]) === null || _h === void 0 ? void 0 : _h.bindCta, (_j = data.heroSection.bindProducts) === null || _j === void 0 ? void 0 : _j[0], baseStyles.heroButton, 'heroSection', 0))))),
21456
+ isHeroVideoPaused && (React.createElement(FormatImage$1, { className: 'clc-pb-video-pause', src: videoPlayIcon, alt: 'play' })))) : ((_g = data.heroSection.imgUrls) === null || _g === void 0 ? void 0 : _g[0]) ? (React.createElement("img", { src: data.heroSection.imgUrls[0], alt: 'Hero', style: baseStyles.heroImage })) : null),
21457
+ React.createElement("div", { style: baseStyles.heroOverlay }, renderCTA('heroButton', (_j = (_h = data.heroSection.bindProducts) === null || _h === void 0 ? void 0 : _h[0]) === null || _j === void 0 ? void 0 : _j.bindCta, (_k = data.heroSection.bindProducts) === null || _k === void 0 ? void 0 : _k[0], baseStyles.heroButton, 'heroSection', 0))))),
21090
21458
  data.carouselSection && data.carouselSection.length > 0 && (React.createElement("div", { ref: carouselSectionRef, style: mergeStyles(baseStyles.carouselSection, 'carouselSection', { excludeBorder: true }) },
21091
21459
  React.createElement("div", { style: baseStyles.carouselImageContainer, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd },
21092
21460
  React.createElement("div", { style: Object.assign(Object.assign({}, baseStyles.carouselContainer), { transform: `translateX(-${carouselIndex * 100}%)` }) }, data.carouselSection.map((item, index) => {
@@ -21141,7 +21509,10 @@ const StructurePage = (_a) => {
21141
21509
  });
21142
21510
  el.addEventListener('ended', (e) => {
21143
21511
  console.log('[Carousel Video] ended event triggered, index:', index);
21144
- handleVideoPlayOver(item, 'carouselSection', index, e.currentTarget);
21512
+ handleVideoPlayOver(item, 'carouselSection', index, e.currentTarget, true);
21513
+ });
21514
+ el.addEventListener('timeupdate', (e) => {
21515
+ handleVideoTimeUpdate(item, 'carouselSection', index, e.currentTarget);
21145
21516
  });
21146
21517
  }
21147
21518
  }, src: item.url, style: baseStyles.carouselVideo, muted: true, loop: true, playsInline: true, controls: false, draggable: false, onDragStart: (e) => e.preventDefault(), onLoadedMetadata: () => {
@@ -21174,7 +21545,10 @@ const StructurePage = (_a) => {
21174
21545
  });
21175
21546
  el.addEventListener('ended', (e) => {
21176
21547
  console.log('[Carousel Video (non-current)] ended event triggered, index:', index);
21177
- handleVideoPlayOver(item, 'carouselSection', index, e.currentTarget);
21548
+ handleVideoPlayOver(item, 'carouselSection', index, e.currentTarget, true);
21549
+ });
21550
+ el.addEventListener('timeupdate', (e) => {
21551
+ handleVideoTimeUpdate(item, 'carouselSection', index, e.currentTarget);
21178
21552
  });
21179
21553
  }
21180
21554
  }, src: item.url, style: baseStyles.carouselVideo, muted: true, loop: true, playsInline: true, controls: false, draggable: false, onDragStart: (e) => e.preventDefault(), onLoadedMetadata: () => {
@@ -21197,9 +21571,9 @@ const StructurePage = (_a) => {
21197
21571
  padding: '10px 20px',
21198
21572
  backgroundColor: '#000',
21199
21573
  color: '#fff',
21200
- textAlign: ((_k = multiCTAConfig === null || multiCTAConfig === void 0 ? void 0 : multiCTAConfig.carouselSection) === null || _k === void 0 ? void 0 : _k.textAlign) || 'center'
21574
+ textAlign: ((_l = multiCTAConfig === null || multiCTAConfig === void 0 ? void 0 : multiCTAConfig.carouselSection) === null || _l === void 0 ? void 0 : _l.textAlign) || 'center'
21201
21575
  } },
21202
- ((_l = data.carouselSection[carouselIndex]) === null || _l === void 0 ? void 0 : _l.text) && (React.createElement("div", { style: { marginBottom: '15px' } },
21576
+ ((_m = data.carouselSection[carouselIndex]) === null || _m === void 0 ? void 0 : _m.text) && (React.createElement("div", { style: { marginBottom: '15px' } },
21203
21577
  React.createElement("span", { style: Object.assign({}, (() => {
21204
21578
  const config = multiCTAConfig === null || multiCTAConfig === void 0 ? void 0 : multiCTAConfig.carouselSection;
21205
21579
  if (!config) {
@@ -21235,8 +21609,8 @@ const StructurePage = (_a) => {
21235
21609
  textStyle.margin = config.margin;
21236
21610
  }
21237
21611
  return textStyle;
21238
- })()) }, (_m = data.carouselSection[carouselIndex]) === null || _m === void 0 ? void 0 : _m.text))),
21239
- renderCTA('carouselButton', (_q = (_p = (_o = data.carouselSection[carouselIndex]) === null || _o === void 0 ? void 0 : _o.bindProducts) === null || _p === void 0 ? void 0 : _p[0]) === null || _q === void 0 ? void 0 : _q.bindCta, (_s = (_r = data.carouselSection[carouselIndex]) === null || _r === void 0 ? void 0 : _r.bindProducts) === null || _s === void 0 ? void 0 : _s[0], baseStyles.carouselButton, 'carouselSection', carouselIndex)))),
21612
+ })()) }, (_o = data.carouselSection[carouselIndex]) === null || _o === void 0 ? void 0 : _o.text))),
21613
+ renderCTA('carouselButton', (_r = (_q = (_p = data.carouselSection[carouselIndex]) === null || _p === void 0 ? void 0 : _p.bindProducts) === null || _q === void 0 ? void 0 : _q[0]) === null || _r === void 0 ? void 0 : _r.bindCta, (_t = (_s = data.carouselSection[carouselIndex]) === null || _s === void 0 ? void 0 : _s.bindProducts) === null || _t === void 0 ? void 0 : _t[0], baseStyles.carouselButton, 'carouselSection', carouselIndex)))),
21240
21614
  data.highlightRevealSection && (React.createElement(ProductView, { onExposure: () => handleProductViewExposure(data.highlightRevealSection, 'highlightRevealSection', 0), onLeave: () => handleProductViewLeave(data.highlightRevealSection, 'highlightRevealSection', 0) },
21241
21615
  React.createElement("div", { style: mergeStyles(baseStyles.highlightSection, 'highlightSection', { excludeBorder: true }) },
21242
21616
  React.createElement("div", { style: baseStyles.highlightImageContainer },
@@ -21246,7 +21620,7 @@ const StructurePage = (_a) => {
21246
21620
  padding: '20px',
21247
21621
  backgroundColor: '#000',
21248
21622
  color: '#fff',
21249
- textAlign: ((_t = multiCTAConfig === null || multiCTAConfig === void 0 ? void 0 : multiCTAConfig.highlightSection) === null || _t === void 0 ? void 0 : _t.textAlign) || 'center'
21623
+ textAlign: ((_u = multiCTAConfig === null || multiCTAConfig === void 0 ? void 0 : multiCTAConfig.highlightSection) === null || _u === void 0 ? void 0 : _u.textAlign) || 'center'
21250
21624
  } },
21251
21625
  data.highlightRevealSection.description && (React.createElement("div", { style: { marginBottom: '10px' } },
21252
21626
  React.createElement("span", { style: Object.assign({}, (() => {
@@ -21340,7 +21714,7 @@ const StructurePage = (_a) => {
21340
21714
  padding: '20px',
21341
21715
  backgroundColor: '#000',
21342
21716
  color: '#fff',
21343
- textAlign: ((_u = multiCTAConfig === null || multiCTAConfig === void 0 ? void 0 : multiCTAConfig.footerSection) === null || _u === void 0 ? void 0 : _u.textAlign) || 'center'
21717
+ textAlign: ((_v = multiCTAConfig === null || multiCTAConfig === void 0 ? void 0 : multiCTAConfig.footerSection) === null || _v === void 0 ? void 0 : _v.textAlign) || 'center'
21344
21718
  } },
21345
21719
  data.footerSection.text && (React.createElement("div", { style: { marginBottom: '15px' } },
21346
21720
  React.createElement("span", { style: Object.assign({}, (() => {