@bi-nova/ui-weapp 1.1.3 → 1.2.0-beta.202608131817

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.
@@ -3,7 +3,7 @@
3
3
  * @Author wutao
4
4
  * @Date 2026-04-10
5
5
  * @LastEditors wutao
6
- * @LastEditTime 2026-05-13
6
+ * @LastEditTime 2026-08-12 14:49:58
7
7
  */
8
8
  const {
9
9
  navigateCustomAd
@@ -13,6 +13,7 @@ Component({
13
13
  properties: {
14
14
  unitId: { type: String, value: '' },
15
15
  customAd: { type: Object, value: null },
16
+ priority: { type: String, value: 'wechat-first' },
16
17
  show: { type: Boolean, value: true },
17
18
  zIndex: { type: Number, value: 999 },
18
19
  pictureInPicture: { type: Boolean, value: false },
@@ -27,13 +28,14 @@ Component({
27
28
  mediaUrl: '',
28
29
  posterUrl: '',
29
30
  isImage: true,
30
- adSpec: 2,
31
+ mediaFitMode: 'contain',
31
32
  hasCustomMedia: false,
32
33
  customLoaded: false,
34
+ isFullScreen: false,
33
35
  skipStyle: '',
36
+ disclosureStyle: '',
37
+ combinedControlStyle: '',
34
38
  adBadgeText: '广告',
35
- contentStyle: '',
36
- badgeStyle: '',
37
39
  pipAnimating: false,
38
40
  nativeSplashActive: false
39
41
  },
@@ -42,8 +44,7 @@ Component({
42
44
  /** 系统开屏状态重试次数 */
43
45
  this.checkSplashNum = 0
44
46
  this.initSkipStyle()
45
- this.initSafeAreaStyle()
46
- this.syncAdSource(this.properties.unitId, this.properties.customAd)
47
+ this.syncAdSource(this.properties.unitId, this.properties.customAd, this.properties.priority)
47
48
  this.syncVisible(this.properties.show)
48
49
  },
49
50
  detached() {
@@ -51,8 +52,8 @@ Component({
51
52
  }
52
53
  },
53
54
  observers: {
54
- 'unitId, customAd'(nextUnitId, nextCustomAd) {
55
- this.syncAdSource(nextUnitId, nextCustomAd)
55
+ 'unitId, customAd, priority'(nextUnitId, nextCustomAd, nextPriority) {
56
+ this.syncAdSource(nextUnitId, nextCustomAd, nextPriority)
56
57
  this.syncVisible(this.properties.show)
57
58
  },
58
59
  show(nextShow) {
@@ -125,21 +126,22 @@ Component({
125
126
  * 同步广告来源与媒体数据
126
127
  * @param {string} unitId 微信广告位 ID
127
128
  * @param {Record<string, any> | null} customAd 自定义广告数据
129
+ * @param {string} priority 广告展示优先级
128
130
  * @returns {void}
129
131
  */
130
- syncAdSource(unitId, customAd) {
131
- /** 是否为微信原生广告 */
132
- const isWechatAd = Boolean(unitId)
132
+ syncAdSource(unitId, customAd, priority) {
133
133
  /** 主媒体地址 */
134
134
  const mediaUrl = this.getMediaUrl(customAd)
135
135
  /** 视频封面地址 */
136
136
  const posterUrl = this.getPosterUrl(customAd, mediaUrl)
137
137
  /** 是否存在自定义媒体 */
138
138
  const hasCustomMedia = Boolean(mediaUrl)
139
+ /** 是否优先展示微信广告 */
140
+ const isWechatAd = this.shouldStartWechatAd(unitId, hasCustomMedia, priority)
139
141
  /** 是否为图片媒体 */
140
142
  const isImage = this.getIsImage(customAd, mediaUrl)
141
- /** 广告规格,1 横版,2 竖版 */
142
- const adSpec = this.getAdSpec(customAd)
143
+ /** 自定义素材填充模式 */
144
+ const mediaFitMode = this.getMediaFitMode(customAd)
143
145
  /** 广告角标文案 */
144
146
  const adBadgeText = this.getAdBadgeText(customAd)
145
147
 
@@ -149,11 +151,54 @@ Component({
149
151
  posterUrl,
150
152
  hasCustomMedia,
151
153
  isImage,
152
- adSpec,
154
+ mediaFitMode,
153
155
  adBadgeText
154
156
  })
155
157
  },
156
158
 
159
+ /**
160
+ * 根据广告配置和优先级判断是否首发微信广告
161
+ * @param {string} unitId 微信广告位 ID
162
+ * @param {boolean} hasCustomMedia 是否存在自研素材
163
+ * @param {string} priority 广告展示优先级
164
+ * @returns {boolean}
165
+ */
166
+ shouldStartWechatAd(unitId, hasCustomMedia, priority) {
167
+ if (!unitId) return false
168
+ if (!hasCustomMedia) return true
169
+ return !['custom-first', 'custom-only'].includes(priority)
170
+ },
171
+
172
+ /**
173
+ * 重置当前展示周期的广告来源失败记录
174
+ * @returns {void}
175
+ */
176
+ resetProviderFailures() {
177
+ this.failedProviders = {
178
+ wechat: false,
179
+ custom: false
180
+ }
181
+ },
182
+
183
+ /**
184
+ * 记录指定广告来源加载失败
185
+ * @param {'wechat' | 'custom'} provider 广告来源
186
+ * @returns {void}
187
+ */
188
+ markProviderFailed(provider) {
189
+ if (!this.failedProviders) this.resetProviderFailures()
190
+ this.failedProviders[provider] = true
191
+ },
192
+
193
+ /**
194
+ * 判断指定广告来源是否已经失败
195
+ * @param {'wechat' | 'custom'} provider 广告来源
196
+ * @returns {boolean}
197
+ */
198
+ hasProviderFailed(provider) {
199
+ return Boolean(this.failedProviders && this.failedProviders[provider])
200
+ },
201
+
157
202
  /**
158
203
  * 同步组件显隐状态
159
204
  * @param {boolean} nextShow 是否展示
@@ -162,6 +207,7 @@ Component({
162
207
  syncVisible(nextShow) {
163
208
  if (!nextShow) {
164
209
  this.clearAllTimers()
210
+ this.resetProviderFailures()
165
211
  this.setData({
166
212
  visible: false,
167
213
  showPiP: false,
@@ -173,6 +219,7 @@ Component({
173
219
  return
174
220
  }
175
221
 
222
+ this.resetProviderFailures()
176
223
  this.checkSplashNum = 0
177
224
  this.resetVisibleState()
178
225
  this.checkSplashAdStatus()
@@ -201,7 +248,7 @@ Component({
201
248
  this.startWechatCountDownIfNeeded()
202
249
  })
203
250
 
204
- if (visible && !this.properties.unitId && this.data.hasCustomMedia) {
251
+ if (visible && !this.data.isWechatAd && this.data.hasCustomMedia) {
205
252
  this.startCustomLoadTimeout()
206
253
  }
207
254
  },
@@ -225,7 +272,13 @@ Component({
225
272
  */
226
273
  getMediaUrl(customAd) {
227
274
  if (!customAd) return ''
228
- return customAd.image || customAd.video || ''
275
+
276
+ const mediaType = String(customAd.mediaType || '').trim().toLowerCase()
277
+
278
+ if (mediaType === 'video') return customAd.video || ''
279
+ if (mediaType === 'image') return customAd.image || ''
280
+
281
+ return customAd.video || customAd.image || ''
229
282
  },
230
283
 
231
284
  /**
@@ -247,33 +300,25 @@ Component({
247
300
  */
248
301
  getIsImage(customAd, mediaUrl) {
249
302
  if (!customAd) return true
303
+
304
+ const mediaType = String(customAd.mediaType || '').trim().toLowerCase()
305
+
306
+ if (mediaType === 'video') return false
307
+ if (mediaType === 'image') return true
250
308
  if (customAd.video) return false
251
309
  if (customAd.image) return true
252
310
  return /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(String(mediaUrl || '').trim())
253
311
  },
254
312
 
255
313
  /**
256
- * 获取广告规格
314
+ * 获取自定义素材填充模式
257
315
  * @param {Record<string, any> | null} customAd 自定义广告数据
258
- * @returns {number}
316
+ * @returns {'contain' | 'cover'}
259
317
  */
260
- getAdSpec(customAd) {
261
- if (!customAd) return 2
262
-
263
- /** 显式传入的广告规格 */
264
- const adSpec = Number(customAd.adSpec || customAd.spec || 0)
265
- if (adSpec === 1 || adSpec === 2) return adSpec
266
-
267
- /** 媒体宽度 */
268
- const width = Number(customAd.videoWidth || customAd.width || 0)
269
- /** 媒体高度 */
270
- const height = Number(customAd.videoHeight || customAd.height || 0)
271
-
272
- if (width > 0 && height > 0) {
273
- return width > height ? 1 : 2
274
- }
275
-
276
- return customAd.video ? 1 : 2
318
+ getMediaFitMode(customAd) {
319
+ return customAd && String(customAd.fitMode || '').trim().toLowerCase() === 'cover'
320
+ ? 'cover'
321
+ : 'contain'
277
322
  },
278
323
 
279
324
  /**
@@ -287,7 +332,7 @@ Component({
287
332
  },
288
333
 
289
334
  /**
290
- * 初始化跳过按钮位置样式
335
+ * 初始化开屏顶部控件位置样式
291
336
  * @returns {void}
292
337
  */
293
338
  initSkipStyle() {
@@ -296,7 +341,7 @@ Component({
296
341
  typeof wx.getMenuButtonBoundingClientRect === 'function'
297
342
  ? wx.getMenuButtonBoundingClientRect()
298
343
  : {}
299
- /** @type {{windowWidth?: number, windowHeight?: number, screenHeight?: number}} */
344
+ /** @type {{windowWidth?: number, windowHeight?: number, screenHeight?: number, safeArea?: {left?: number}}} */
300
345
  const windowInfo =
301
346
  typeof wx.getWindowInfo === 'function'
302
347
  ? wx.getWindowInfo()
@@ -305,7 +350,11 @@ Component({
305
350
  : {}
306
351
 
307
352
  /** 是否可按原生菜单胶囊对齐 */
308
- const isFullScreen = windowInfo.windowHeight === windowInfo.screenHeight
353
+ const isFullScreen = Boolean(
354
+ windowInfo.windowHeight
355
+ && windowInfo.screenHeight
356
+ && windowInfo.windowHeight === windowInfo.screenHeight
357
+ )
309
358
  /** 默认顶部距离 */
310
359
  const defaultTop = 16
311
360
  /** 默认右侧距离 */
@@ -320,46 +369,32 @@ Component({
320
369
  const height = isFullScreen && menuButtonRect.height ? menuButtonRect.height : defaultHeight
321
370
  /** 跳过按钮水平定位样式 */
322
371
  const horizontalStyle = isFullScreen ? `left:${defaultLeft}px` : `right:${defaultRight}px`
372
+ /** 广告标识顶部偏移 */
373
+ const disclosureTop = top
374
+ /** 广告标识左侧偏移,兼容安全区域可能存在的水平边距 */
375
+ const disclosureLeft = Math.max(defaultLeft, Number(windowInfo.safeArea && windowInfo.safeArea.left || 0) + defaultLeft)
323
376
 
324
377
  this.setData({
378
+ isFullScreen,
325
379
  skipStyle: [
326
380
  `top:${top}px`,
327
381
  horizontalStyle,
328
382
  `height:${height}px`,
329
383
  `line-height:${height}px`,
330
384
  `border-radius:${Math.round(height / 2)}px`
385
+ ].join(';'),
386
+ disclosureStyle: [
387
+ `top:${disclosureTop}px`,
388
+ `left:${disclosureLeft}px`
389
+ ].join(';'),
390
+ combinedControlStyle: [
391
+ `top:${top}px`,
392
+ `left:${disclosureLeft}px`,
393
+ `height:${height}px`
331
394
  ].join(';')
332
395
  })
333
396
  },
334
397
 
335
- /**
336
- * 初始化安全区相关样式
337
- * @returns {void}
338
- */
339
- initSafeAreaStyle() {
340
- /** @type {{safeArea?: {bottom?: number}, screenHeight?: number, windowHeight?: number}} */
341
- const systemInfo =
342
- typeof wx.getWindowInfo === 'function'
343
- ? wx.getWindowInfo()
344
- : typeof wx.getSystemInfoSync === 'function'
345
- ? wx.getSystemInfoSync()
346
- : {}
347
- /** 底部安全区高度 */
348
- const safeBottom = Math.max(
349
- 0,
350
- Number(systemInfo.screenHeight || 0) - Number(systemInfo.safeArea && systemInfo.safeArea.bottom || systemInfo.windowHeight || 0)
351
- )
352
- /** 内容区底部间距 */
353
- const contentBottom = safeBottom > 0 ? safeBottom + 42 : 84
354
- /** 角标底部间距 */
355
- const badgeBottom = safeBottom > 0 ? safeBottom + 88 : 130
356
-
357
- this.setData({
358
- contentStyle: `bottom:${contentBottom}px;`,
359
- badgeStyle: `bottom:${badgeBottom}px;`
360
- })
361
- },
362
-
363
398
  /**
364
399
  * 启动广告倒计时
365
400
  * @returns {void}
@@ -473,11 +508,68 @@ Component({
473
508
  */
474
509
  startWechatCountDownIfNeeded() {
475
510
  // 微信原生广告 load 回调在重新编译、缓存恢复等场景下可能不稳定;只要原生广告容器已展示,就允许兜底启动倒计时。
476
- if (!this.properties.show || !this.properties.unitId || !this.data.visible || this.data.showPiP) return
511
+ if (!this.properties.show || !this.properties.unitId || !this.data.isWechatAd || !this.data.visible || this.data.showPiP) return
477
512
 
478
513
  this.startCountDown()
479
514
  },
480
515
 
516
+ /**
517
+ * 切换当前展示的广告来源
518
+ * @param {'wechat' | 'custom'} provider 目标广告来源
519
+ * @returns {void}
520
+ */
521
+ switchProvider(provider) {
522
+ const isWechatAd = provider === 'wechat'
523
+
524
+ this.clearAllTimers()
525
+ this.setData({
526
+ isWechatAd,
527
+ visible: true,
528
+ showPiP: false,
529
+ remainingSeconds: this.getJumpSeconds(),
530
+ customLoaded: false,
531
+ pipAnimating: false
532
+ }, () => {
533
+ if (isWechatAd) {
534
+ this.startWechatCountDownIfNeeded()
535
+ return
536
+ }
537
+
538
+ this.startCustomLoadTimeout()
539
+ })
540
+ },
541
+
542
+ /**
543
+ * 所有可用广告来源均失败后结束当前开屏流程
544
+ * @param {'wechat' | 'custom'} provider 最后失败的广告来源
545
+ * @param {Record<string, any>} errorDetail 错误详情
546
+ * @returns {void}
547
+ */
548
+ finishProviderFailure(provider, errorDetail) {
549
+ const hasWechatAd = Boolean(this.properties.unitId)
550
+ const hasCustomAd = Boolean(this.data.hasCustomMedia)
551
+ const reason = hasWechatAd && hasCustomAd
552
+ ? 'allProvidersFailed'
553
+ : provider === 'wechat'
554
+ ? 'wechatAdLoadFail'
555
+ : 'customMediaLoadFail'
556
+
557
+ this.clearAllTimers()
558
+ this.setData({
559
+ visible: false,
560
+ showPiP: false,
561
+ remainingSeconds: 0,
562
+ customLoaded: false,
563
+ pipAnimating: false
564
+ })
565
+ this.triggerEvent('close', {
566
+ provider,
567
+ type: 'splash',
568
+ reason,
569
+ error: errorDetail
570
+ })
571
+ },
572
+
481
573
  /**
482
574
  * 设置开屏广告可见状态并按需启动微信原生广告倒计时
483
575
  * @param {boolean} visible 是否展示开屏广告
@@ -554,6 +646,8 @@ Component({
554
646
  * @returns {void}
555
647
  */
556
648
  handleLoad(event) {
649
+ if (!this.data.isWechatAd) return
650
+
557
651
  this.startCountDown()
558
652
  this.triggerEvent('load', this.resolveEventDetail('wechat', event))
559
653
  },
@@ -564,6 +658,8 @@ Component({
564
658
  * @returns {void}
565
659
  */
566
660
  handleError(event) {
661
+ if (!this.data.isWechatAd) return
662
+
567
663
  const rawDetail = event && event.detail && typeof event.detail === 'object' ? event.detail : {}
568
664
  const errorDetail = this.resolveEventDetail('wechat', event, {
569
665
  errCode: 'BI_SPLASH_WECHAT_AD_LOAD_FAIL',
@@ -584,34 +680,14 @@ Component({
584
680
  fallbackFromWechatAd(errorDetail) {
585
681
  if (!this.data.isWechatAd) return
586
682
 
587
- this.clearAllTimers()
683
+ this.markProviderFailed('wechat')
588
684
 
589
- if (this.data.hasCustomMedia) {
590
- this.setData({
591
- isWechatAd: false,
592
- visible: true,
593
- showPiP: false,
594
- remainingSeconds: this.getJumpSeconds(),
595
- customLoaded: false,
596
- pipAnimating: false
597
- })
598
- this.startCustomLoadTimeout()
685
+ if (this.data.hasCustomMedia && !this.hasProviderFailed('custom')) {
686
+ this.switchProvider('custom')
599
687
  return
600
688
  }
601
689
 
602
- this.setData({
603
- visible: false,
604
- showPiP: false,
605
- remainingSeconds: 0,
606
- customLoaded: false,
607
- pipAnimating: false
608
- })
609
- this.triggerEvent('close', {
610
- provider: 'wechat',
611
- type: 'splash',
612
- reason: 'wechatAdLoadFail',
613
- error: errorDetail
614
- })
690
+ this.finishProviderFailure('wechat', errorDetail)
615
691
  },
616
692
 
617
693
  /**
@@ -619,7 +695,7 @@ Component({
619
695
  * @returns {void}
620
696
  */
621
697
  handleCustomLoad() {
622
- if (this.data.customLoaded) return
698
+ if (this.data.isWechatAd || this.data.customLoaded) return
623
699
 
624
700
  this.clearCustomLoadTimeout()
625
701
  this.setData({ customLoaded: true })
@@ -647,15 +723,19 @@ Component({
647
723
  * @returns {void}
648
724
  */
649
725
  handleCustomMediaFail(event, detail) {
650
- this.clearAllTimers()
651
- this.setData({
652
- visible: false,
653
- showPiP: false,
654
- remainingSeconds: 0,
655
- customLoaded: false,
656
- pipAnimating: false
657
- })
658
- this.triggerEvent('error', this.resolveEventDetail('custom', event, detail))
726
+ if (this.data.isWechatAd) return
727
+
728
+ const errorDetail = this.resolveEventDetail('custom', event, detail)
729
+
730
+ this.triggerEvent('error', errorDetail)
731
+ this.markProviderFailed('custom')
732
+
733
+ if (this.properties.unitId && !this.hasProviderFailed('wechat')) {
734
+ this.switchProvider('wechat')
735
+ return
736
+ }
737
+
738
+ this.finishProviderFailure('custom', errorDetail)
659
739
  },
660
740
 
661
741
  /**
@@ -18,30 +18,25 @@
18
18
  </view>
19
19
 
20
20
  <!-- 自定义广告 -->
21
- <view wx:elif="{{mediaUrl}}" class="bi-splash-ad__popup">
21
+ <view wx:elif="{{mediaUrl}}" class="bi-splash-ad__popup bi-splash-ad__custom">
22
22
  <view class="bi-splash-ad__media-shell" bindtap="handleAction">
23
23
  <image
24
24
  wx:if="{{isImage}}"
25
- class="bi-splash-ad__media bi-splash-ad__media--background"
25
+ class="bi-splash-ad__media"
26
26
  src="{{mediaUrl}}"
27
- mode="aspectFill"
27
+ mode="{{mediaFitMode === 'cover' ? 'aspectFill' : 'aspectFit'}}"
28
28
  bindload="handleCustomLoad"
29
29
  binderror="handleCustomMediaError"
30
30
  />
31
31
  <block wx:else>
32
- <image
33
- class="bi-splash-ad__media bi-splash-ad__media--background"
34
- src="{{posterUrl}}"
35
- mode="aspectFill"
36
- />
37
32
  <video
38
- class="bi-splash-ad__video {{adSpec === 1 ? 'bi-splash-ad__video--landscape' : ''}}"
33
+ class="bi-splash-ad__video"
39
34
  src="{{mediaUrl}}"
40
35
  poster="{{posterUrl}}"
41
36
  autoplay="{{true}}"
42
37
  muted="{{customAd.muted !== false}}"
43
38
  loop="{{true}}"
44
- object-fit="cover"
39
+ object-fit="{{mediaFitMode}}"
45
40
  show-center-play-btn="{{false}}"
46
41
  show-play-btn="{{false}}"
47
42
  controls="{{false}}"
@@ -52,13 +47,17 @@
52
47
 
53
48
  <view wx:if="{{showPiP}}" class="bi-splash-ad__pip-mask"></view>
54
49
 
55
- <view wx:if="{{!showPiP}}" class="bi-splash-ad__badge" style="{{badgeStyle}}">{{adBadgeText}}</view>
56
-
57
- <view wx:if="{{customAd && (customAd.title || (!showPiP && (customAd.description || customAd.buttonText)))}}" class="bi-splash-ad__content" style="{{contentStyle}}">
58
- <text wx:if="{{customAd.title}}" class="bi-splash-ad__title">{{customAd.title}}</text>
59
- <text wx:if="{{!showPiP && customAd.description}}" class="bi-splash-ad__description">{{customAd.description}}</text>
60
- <button wx:if="{{!showPiP && customAd.buttonText}}" class="bi-splash-ad__action">{{customAd.buttonText}}</button>
50
+ <view wx:if="{{!showPiP && !isFullScreen}}" class="bi-splash-ad__disclosure" style="{{disclosureStyle}}">
51
+ <text>{{adBadgeText}}</text>
52
+ <view class="bi-splash-ad__disclosure-arrow"></view>
61
53
  </view>
54
+
55
+ <image
56
+ wx:if="{{!showPiP && customAd.bottomImage}}"
57
+ class="bi-splash-ad__bottom-image"
58
+ src="{{customAd.bottomImage}}"
59
+ mode="widthFix"
60
+ />
62
61
  </view>
63
62
  </view>
64
63
 
@@ -66,7 +65,23 @@
66
65
  <view class="bi-splash-ad__pip-close-text">关闭</view>
67
66
  </view>
68
67
 
69
- <view wx:if="{{!showPiP}}" class="bi-splash-ad__skip" style="{{skipStyle}}" bindtap="handleSkip">
68
+ <view
69
+ wx:if="{{!showPiP && isFullScreen && !isWechatAd}}"
70
+ class="bi-splash-ad__combined-control"
71
+ style="{{combinedControlStyle}}"
72
+ >
73
+ <view class="bi-splash-ad__combined-capsule">
74
+ <view class="bi-splash-ad__combined-disclosure">
75
+ <text>{{adBadgeText}}</text>
76
+ <view class="bi-splash-ad__disclosure-arrow"></view>
77
+ </view>
78
+ <view class="bi-splash-ad__combined-divider"></view>
79
+ <view class="bi-splash-ad__combined-skip" catchtap="handleSkip">跳过</view>
80
+ </view>
81
+ <text class="bi-splash-ad__combined-countdown">{{remainingSeconds}} 秒</text>
82
+ </view>
83
+
84
+ <view wx:elif="{{!showPiP}}" class="bi-splash-ad__skip" style="{{skipStyle}}" bindtap="handleSkip">
70
85
  <text>{{remainingSeconds}}s</text>
71
86
  <view class="bi-splash-ad__skip-divider"></view>
72
87
  <text class="bi-splash-ad__skip-text">跳过</text>