@bi-nova/ui-weapp 1.1.1-beta.202605161224

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.
@@ -0,0 +1,132 @@
1
+ .bi-banner-ad {
2
+ position: relative;
3
+ overflow: hidden;
4
+ border-radius: 16rpx;
5
+ background: #ffffff;
6
+ box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.08);
7
+ }
8
+
9
+ .bi-banner-ad__surface {
10
+ display: flex;
11
+ align-items: stretch;
12
+ min-height: 156rpx;
13
+ }
14
+
15
+ .bi-banner-ad__media {
16
+ position: relative;
17
+ flex: 0 0 132rpx;
18
+ width: 132rpx;
19
+ height: 156rpx;
20
+ overflow: hidden;
21
+ }
22
+
23
+ .bi-banner-ad__image {
24
+ display: block;
25
+ width: 100%;
26
+ height: 100%;
27
+ }
28
+
29
+ .bi-banner-ad__content {
30
+ display: flex;
31
+ flex: 1;
32
+ flex-direction: column;
33
+ justify-content: space-between;
34
+ min-width: 0;
35
+ padding: 18rpx 20rpx 18rpx 18rpx;
36
+ }
37
+
38
+ .bi-banner-ad__badge {
39
+ display: inline-flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ padding: 0 10rpx;
43
+ height: 30rpx;
44
+ border-radius: 6rpx;
45
+ background: rgba(0, 0, 0, 0.6);
46
+ color: #ffffff;
47
+ font-size: 18rpx;
48
+ line-height: 30rpx;
49
+ }
50
+
51
+ .bi-banner-ad__badge--overlay {
52
+ position: absolute;
53
+ top: 8rpx;
54
+ left: 8rpx;
55
+ z-index: 1;
56
+ }
57
+
58
+ .bi-banner-ad__body {
59
+ flex: 1;
60
+ min-width: 0;
61
+ overflow: hidden;
62
+ }
63
+
64
+ .bi-banner-ad__title {
65
+ display: -webkit-box;
66
+ color: #0f172a;
67
+ font-size: 30rpx;
68
+ font-weight: 500;
69
+ line-height: 1.35;
70
+ overflow: hidden;
71
+ text-overflow: ellipsis;
72
+ -webkit-box-orient: vertical;
73
+ -webkit-line-clamp: 2;
74
+ }
75
+
76
+ .bi-banner-ad__description {
77
+ flex: 1;
78
+ min-width: 0;
79
+ margin-right: 16rpx;
80
+ color: #8c8c8c;
81
+ font-size: 22rpx;
82
+ line-height: 1.4;
83
+ white-space: nowrap;
84
+ overflow: hidden;
85
+ text-overflow: ellipsis;
86
+ }
87
+
88
+ .bi-banner-ad__footer {
89
+ display: flex;
90
+ align-items: center;
91
+ margin-top: 14rpx;
92
+ }
93
+
94
+ .bi-banner-ad__button {
95
+ flex: 0 0 auto;
96
+ min-width: 144rpx;
97
+ min-height: 0;
98
+ padding: 0 20rpx;
99
+ border: none;
100
+ border-radius: 12rpx;
101
+ background: #18b566;
102
+ color: #ffffff;
103
+ font-size: 24rpx;
104
+ font-weight: 500;
105
+ line-height: 54rpx;
106
+ }
107
+
108
+ .bi-banner-ad__button::after {
109
+ display: none;
110
+ }
111
+
112
+ .bi-banner-ad__close {
113
+ position: absolute;
114
+ top: 10rpx;
115
+ right: 10rpx;
116
+ width: 36rpx;
117
+ height: 36rpx;
118
+ min-height: 0;
119
+ padding: 0;
120
+ border: none;
121
+ border-radius: 999rpx;
122
+ background: rgba(15, 23, 42, 0.35);
123
+ color: #ffffff;
124
+ font-size: 24rpx;
125
+ line-height: 36rpx;
126
+ text-align: center;
127
+ z-index: 1;
128
+ }
129
+
130
+ .bi-banner-ad__close::after {
131
+ display: none;
132
+ }
@@ -0,0 +1,496 @@
1
+ /**
2
+ * @Description 展示广告顶层分发组件
3
+ * @Author wutao
4
+ * @Date 2026-04-10
5
+ * @LastEditors wutao
6
+ * @LastEditTime 2026-05-08
7
+ */
8
+ const SUPPORTED_TYPES = ['banner', 'splash']
9
+ /** 微信系统开屏广告承接记录的本地缓存键前缀,用于按 appId 和广告位隔离缓存 */
10
+ const NATIVE_SPLASH_CONSUMED_STORAGE_PREFIX = 'BI_DISPLAY_NATIVE_SPLASH_CONSUMED'
11
+ /** 微信系统开屏广告承接记录有效期,超过5分钟后重新允许微信封面优先检查 */
12
+ const NATIVE_SPLASH_CONSUMED_CACHE_DURATION = 5 * 60 * 1000
13
+
14
+ Component({
15
+ properties: {
16
+ adId: { type: String, value: '' },
17
+ unitId: { type: String, value: '' },
18
+ type: { type: String, value: 'banner' },
19
+ customAd: { type: Object, value: null },
20
+ adOptions: { type: Object, value: null },
21
+ sdkKey: { type: String, value: 'biNova' },
22
+ autoLoad: { type: Boolean, value: true },
23
+ adIntervals: { type: Number, value: 30 },
24
+ show: { type: Boolean, value: true },
25
+ zIndex: { type: Number, value: 999 },
26
+ pictureInPicture: { type: Boolean, value: false },
27
+ checkNativeSplash: { type: Boolean, value: true },
28
+ nativeSplashCache: { type: Boolean, value: true },
29
+ jumpSeconds: { type: Number, value: 5 }
30
+ },
31
+ data: {
32
+ resolvedType: 'banner',
33
+ resolvedUnitId: '',
34
+ resolvedCustomAd: null,
35
+ resolvedShow: true,
36
+ remoteLoading: false,
37
+ remoteLoaded: false,
38
+ remoteAdData: null,
39
+ resolvedCheckNativeSplash: true
40
+ },
41
+ lifetimes: {
42
+ attached() {
43
+ this.syncType(this.properties.type)
44
+ this.syncNativeSplashCheck()
45
+ this.syncResolvedAdData()
46
+ this.refreshRemoteConfig('attached')
47
+ },
48
+ detached() {
49
+ this.requestToken = 0
50
+ }
51
+ },
52
+ observers: {
53
+ type(nextType) {
54
+ this.syncType(nextType)
55
+ this.syncResolvedAdData()
56
+ },
57
+ 'unitId, customAd, show'(nextUnitId, nextCustomAd, nextShow) {
58
+ this.syncResolvedAdData({
59
+ unitId: nextUnitId,
60
+ customAd: nextCustomAd,
61
+ show: nextShow
62
+ })
63
+ },
64
+ 'adId, adOptions, sdkKey, autoLoad'(nextAdId) {
65
+ this.syncNativeSplashCheck()
66
+
67
+ if (!nextAdId) {
68
+ this.clearRemoteState()
69
+ this.syncResolvedAdData()
70
+ return
71
+ }
72
+
73
+ this.refreshRemoteConfig('observer')
74
+ },
75
+ checkNativeSplash() {
76
+ this.syncNativeSplashCheck()
77
+ },
78
+ nativeSplashCache() {
79
+ this.syncNativeSplashCheck()
80
+ }
81
+ },
82
+ methods: {
83
+ /**
84
+ * 获取微信系统开屏广告承接缓存键
85
+ * @param {string} adId 广告位 ID
86
+ * @returns {string}
87
+ */
88
+ getNativeSplashConsumedStorageKey(adId) {
89
+ const appId = this.getMiniProgramAppId()
90
+ return `${NATIVE_SPLASH_CONSUMED_STORAGE_PREFIX}_${appId || 'unknown'}_${adId || 'empty'}`
91
+ },
92
+
93
+ /**
94
+ * 读取微信系统开屏广告是否已经承接过当前广告位
95
+ * @param {string} adId 广告位 ID
96
+ * @returns {boolean}
97
+ */
98
+ getNativeSplashConsumed(adId) {
99
+ if (!this.properties.nativeSplashCache) return false
100
+ if (!adId || typeof wx === 'undefined' || typeof wx.getStorageSync !== 'function') return false
101
+
102
+ try {
103
+ const consumedTime = Number(wx.getStorageSync(this.getNativeSplashConsumedStorageKey(adId)) || 0)
104
+ return consumedTime > 0 && Date.now() - consumedTime <= NATIVE_SPLASH_CONSUMED_CACHE_DURATION
105
+ } catch (error) {
106
+ return false
107
+ }
108
+ },
109
+
110
+ /**
111
+ * 记录微信系统开屏广告已经承接过当前广告位
112
+ * @returns {void}
113
+ */
114
+ setNativeSplashConsumed() {
115
+ if (!this.properties.nativeSplashCache) return
116
+ if (!this.properties.adId || typeof wx === 'undefined' || typeof wx.setStorageSync !== 'function') return
117
+
118
+ try {
119
+ wx.setStorageSync(this.getNativeSplashConsumedStorageKey(this.properties.adId), Date.now())
120
+ } catch (error) {}
121
+ },
122
+
123
+ /**
124
+ * 同步是否需要检查微信系统开屏广告
125
+ * @returns {void}
126
+ */
127
+ syncNativeSplashCheck() {
128
+ this.setData({
129
+ resolvedCheckNativeSplash: Boolean(this.properties.checkNativeSplash && !this.getNativeSplashConsumed(this.properties.adId))
130
+ })
131
+ },
132
+
133
+ /**
134
+ * 判断是否启用远程广告配置
135
+ * @returns {boolean}
136
+ */
137
+ shouldUseRemoteConfig() {
138
+ return Boolean(this.properties.autoLoad && this.properties.adId)
139
+ },
140
+
141
+ /**
142
+ * 获取全局 SDK 实例
143
+ * @returns {Record<string, any> | null}
144
+ */
145
+ getSdkInstance() {
146
+ const app = typeof getApp === 'function' ? getApp() : null
147
+
148
+ if (!app) return null
149
+
150
+ const sdkKey = this.properties.sdkKey || 'biNova'
151
+ return app[sdkKey] || app.globalData && app.globalData[sdkKey] || null
152
+ },
153
+
154
+ /**
155
+ * 获取当前小程序 appId
156
+ * @returns {string}
157
+ */
158
+ getMiniProgramAppId() {
159
+ try {
160
+ if (typeof wx.getAccountInfoSync === 'function') {
161
+ const accountInfo = wx.getAccountInfoSync()
162
+ return accountInfo && accountInfo.miniProgram && accountInfo.miniProgram.appId || ''
163
+ }
164
+ } catch (error) {
165
+ return ''
166
+ }
167
+
168
+ return ''
169
+ },
170
+
171
+ /**
172
+ * 清理远程广告状态
173
+ * @returns {void}
174
+ */
175
+ clearRemoteState() {
176
+ /** 把请求版本号往前推进一位,让之前未完成的异步请求全部过期,避免旧请求回来后误更新组件状态 */
177
+ this.requestToken = (this.requestToken || 0) + 1
178
+ this.lastRemoteRequestKey = ''
179
+ this.setData({
180
+ remoteLoading: false,
181
+ remoteLoaded: false,
182
+ remoteAdData: null
183
+ })
184
+ },
185
+
186
+ /**
187
+ * 构建远程广告请求参数
188
+ * @returns {Record<string, any>}
189
+ */
190
+ buildRemoteRequestOptions() {
191
+ const adOptions = this.properties.adOptions && typeof this.properties.adOptions === 'object'
192
+ ? this.properties.adOptions
193
+ : {}
194
+ const appId = this.getMiniProgramAppId()
195
+
196
+ return {
197
+ ...adOptions,
198
+ adId: this.properties.adId,
199
+ placementId: adOptions.placementId || this.properties.adId,
200
+ appId: adOptions.appId || appId,
201
+ type: this.data.resolvedType
202
+ }
203
+ },
204
+
205
+ /**
206
+ * 获取远程广告请求去重键
207
+ * @param {Record<string, any>} requestOptions 请求参数
208
+ * @returns {string}
209
+ */
210
+ getRemoteRequestKey(requestOptions) {
211
+ try {
212
+ return JSON.stringify(requestOptions || {})
213
+ } catch (error) {
214
+ return `${requestOptions && requestOptions.appId || ''}_${requestOptions && requestOptions.placementId || ''}`
215
+ }
216
+ },
217
+
218
+ /**
219
+ * 解析远程广告数据
220
+ * @param {Record<string, any>} remoteData 远程广告配置
221
+ * @returns {{unitId: string, customAd: Record<string, any> | null, show: boolean}}
222
+ */
223
+ resolveRemoteAdData(remoteData) {
224
+ const unitId = remoteData && remoteData.unitId || ''
225
+ const customAd = remoteData && (remoteData.customAd || remoteData.custom) || null
226
+ const show = remoteData && typeof remoteData.show === 'boolean'
227
+ ? remoteData.show
228
+ : Boolean(unitId || customAd)
229
+
230
+ return {
231
+ unitId,
232
+ customAd,
233
+ show
234
+ }
235
+ },
236
+
237
+ /**
238
+ * 同步组件最终消费的广告数据
239
+ * @param {{unitId?: string, customAd?: Record<string, any> | null, show?: boolean}} [options] 可选覆盖参数
240
+ * @returns {void}
241
+ */
242
+ syncResolvedAdData(options) {
243
+ const nextOptions = options || {}
244
+ const remoteEnabled = this.shouldUseRemoteConfig()
245
+ const remoteAdData = remoteEnabled ? this.data.remoteAdData : null
246
+ const remoteResolved = remoteAdData ? this.resolveRemoteAdData(remoteAdData) : null
247
+ const unitId = remoteResolved ? remoteResolved.unitId : (nextOptions.unitId !== undefined ? nextOptions.unitId : this.properties.unitId)
248
+ const customAd = remoteResolved ? remoteResolved.customAd : (nextOptions.customAd !== undefined ? nextOptions.customAd : this.properties.customAd)
249
+ const show = nextOptions.show !== undefined ? nextOptions.show : this.properties.show
250
+ const resolvedShow = Boolean(show && (!remoteEnabled || !this.properties.adId || !this.data.remoteLoaded || remoteResolved && remoteResolved.show))
251
+
252
+ this.setData({
253
+ resolvedUnitId: unitId || '',
254
+ resolvedCustomAd: customAd || null,
255
+ resolvedShow
256
+ })
257
+ },
258
+
259
+ /**
260
+ * 触发配置错误事件
261
+ * @param {Record<string, any>} detail 错误详情
262
+ * @returns {void}
263
+ */
264
+ triggerConfigError(detail) {
265
+ this.triggerEvent('configerror', detail)
266
+ this.triggerEvent('error', detail)
267
+ },
268
+
269
+ /**
270
+ * 拉取远程广告配置
271
+ * @param {string} reason 触发原因
272
+ * @returns {Promise<void>}
273
+ */
274
+ async refreshRemoteConfig(reason) {
275
+ if (!this.shouldUseRemoteConfig()) {
276
+ this.syncResolvedAdData()
277
+ return
278
+ }
279
+
280
+ const requestOptions = this.buildRemoteRequestOptions()
281
+ const requestKey = this.getRemoteRequestKey(requestOptions)
282
+
283
+ if (this.lastRemoteRequestKey === requestKey && (this.data.remoteLoading || this.data.remoteLoaded)) {
284
+ // console.info('[BiDisplayAd] 跳过重复广告配置请求', {
285
+ // reason,
286
+ // requestOptions
287
+ // })
288
+ return
289
+ }
290
+
291
+ const sdk = this.getSdkInstance()
292
+
293
+ if (!sdk || !sdk.ads || typeof sdk.ads.getDisplayAdConfig !== 'function') {
294
+ this.clearRemoteState()
295
+ this.syncResolvedAdData({
296
+ unitId: '',
297
+ customAd: null,
298
+ show: false
299
+ })
300
+ this.triggerConfigError({
301
+ type: this.data.resolvedType,
302
+ adId: this.properties.adId,
303
+ errCode: 'DISPLAY_AD_SDK_NOT_READY',
304
+ errMsg: '未获取到可用的广告服务 SDK,请先注入全局 biNova 实例'
305
+ })
306
+ return
307
+ }
308
+
309
+ const requestToken = (this.requestToken || 0) + 1
310
+ this.requestToken = requestToken
311
+ this.lastRemoteRequestKey = requestKey
312
+
313
+ // console.info('[BiDisplayAd] 开始获取广告配置', {
314
+ // reason,
315
+ // requestOptions
316
+ // })
317
+
318
+ this.setData({
319
+ remoteLoading: true,
320
+ remoteLoaded: false,
321
+ remoteAdData: null
322
+ })
323
+ this.syncResolvedAdData({
324
+ unitId: '',
325
+ customAd: null,
326
+ show: false
327
+ })
328
+
329
+ try {
330
+ const result = await sdk.ads.getDisplayAdConfig(requestOptions)
331
+
332
+ if (requestToken !== this.requestToken) return
333
+
334
+ // console.info('[BiDisplayAd] 广告配置接口返回', result)
335
+
336
+ if (!result || result.code !== 0 || !result.data) {
337
+ this.setData({
338
+ remoteLoading: false,
339
+ remoteLoaded: true,
340
+ remoteAdData: null
341
+ })
342
+ this.syncResolvedAdData({
343
+ unitId: '',
344
+ customAd: null,
345
+ show: false
346
+ })
347
+ this.triggerConfigError({
348
+ type: this.data.resolvedType,
349
+ adId: this.properties.adId,
350
+ errCode: 'DISPLAY_AD_REMOTE_CONFIG_FAIL',
351
+ errMsg: result && result.message || '获取展示广告配置失败',
352
+ rawResult: result || null
353
+ })
354
+ return
355
+ }
356
+
357
+ this.setData({
358
+ remoteLoading: false,
359
+ remoteLoaded: true,
360
+ remoteAdData: result.data
361
+ })
362
+ this.syncResolvedAdData()
363
+ this.triggerEvent('configload', {
364
+ type: this.data.resolvedType,
365
+ adId: this.properties.adId,
366
+ requestOptions,
367
+ data: result.data
368
+ })
369
+ } catch (error) {
370
+ if (requestToken !== this.requestToken) return
371
+
372
+ console.error('[BiDisplayAd] 广告配置请求异常', error)
373
+
374
+ this.setData({
375
+ remoteLoading: false,
376
+ remoteLoaded: true,
377
+ remoteAdData: null
378
+ })
379
+ this.syncResolvedAdData({
380
+ unitId: '',
381
+ customAd: null,
382
+ show: false
383
+ })
384
+ this.triggerConfigError({
385
+ type: this.data.resolvedType,
386
+ adId: this.properties.adId,
387
+ errCode: 'DISPLAY_AD_REMOTE_CONFIG_EXCEPTION',
388
+ errMsg: error && error.message || '获取展示广告配置异常',
389
+ error
390
+ })
391
+ }
392
+ },
393
+
394
+ /**
395
+ * 统一透传子组件事件
396
+ * @param {'load' | 'error' | 'close' | 'action' | 'hide' | 'openWebview'} eventName 事件名称
397
+ * @param {Record<string, any>} event 原始事件对象
398
+ * @returns {void}
399
+ */
400
+ forwardEvent(eventName, event) {
401
+ const detail = event && event.detail && typeof event.detail === 'object'
402
+ ? event.detail
403
+ : event && typeof event === 'object'
404
+ ? event
405
+ : {}
406
+
407
+ this.triggerEvent(eventName, {
408
+ adId: this.properties.adId || this.data.remoteAdData && this.data.remoteAdData.adId || '',
409
+ ...detail
410
+ })
411
+ },
412
+
413
+ /**
414
+ * 同步广告类型
415
+ * @param {string} nextType 广告类型
416
+ * @returns {void}
417
+ */
418
+ syncType(nextType) {
419
+ if (SUPPORTED_TYPES.includes(nextType)) {
420
+ this.setData({ resolvedType: nextType })
421
+ return
422
+ }
423
+
424
+ this.setData({ resolvedType: 'banner' })
425
+ this.triggerEvent('error', {
426
+ type: 'banner',
427
+ errCode: 'UNSUPPORTED_DISPLAY_AD_TYPE',
428
+ errMsg: `不支持的展示广告类型:${nextType},请使用 "banner" 或 "splash"`
429
+ })
430
+ },
431
+
432
+ /**
433
+ * 处理广告加载完成事件
434
+ * @param {Record<string, any>} event 广告加载事件
435
+ * @returns {void}
436
+ */
437
+ handleLoad(event) {
438
+ const detail = event && event.detail && typeof event.detail === 'object'
439
+ ? event.detail
440
+ : event && typeof event === 'object'
441
+ ? event
442
+ : {}
443
+
444
+ if (detail.nativeSplash) {
445
+ this.setNativeSplashConsumed()
446
+ }
447
+
448
+ this.forwardEvent('load', event)
449
+ },
450
+
451
+ /**
452
+ * 处理广告错误事件
453
+ * @param {Record<string, any>} event 广告错误事件
454
+ * @returns {void}
455
+ */
456
+ handleError(event) {
457
+ this.forwardEvent('error', event)
458
+ },
459
+
460
+ /**
461
+ * 处理广告关闭事件
462
+ * @param {Record<string, any>} event 广告关闭事件
463
+ * @returns {void}
464
+ */
465
+ handleClose(event) {
466
+ this.forwardEvent('close', event)
467
+ },
468
+
469
+ /**
470
+ * 处理广告点击事件
471
+ * @param {Record<string, any>} event 广告点击事件
472
+ * @returns {void}
473
+ */
474
+ handleAction(event) {
475
+ this.forwardEvent('action', event)
476
+ },
477
+
478
+ /**
479
+ * 处理广告隐藏事件
480
+ * @param {Record<string, any>} event 广告隐藏事件
481
+ * @returns {void}
482
+ */
483
+ handleHide(event) {
484
+ this.forwardEvent('hide', event)
485
+ },
486
+
487
+ /**
488
+ * 处理打开 WebView 事件
489
+ * @param {Record<string, any>} event WebView 打开事件
490
+ * @returns {void}
491
+ */
492
+ handleOpenWebview(event) {
493
+ this.forwardEvent('openWebview', event)
494
+ }
495
+ }
496
+ })
@@ -0,0 +1,7 @@
1
+ {
2
+ "component": true,
3
+ "usingComponents": {
4
+ "bi-banner-ad": "../banner/index",
5
+ "bi-splash-ad": "../splash/index"
6
+ }
7
+ }
@@ -0,0 +1,30 @@
1
+ <!-- Banner 广告组件 -->
2
+ <bi-banner-ad
3
+ wx:if="{{resolvedType === 'banner'}}"
4
+ unit-id="{{resolvedUnitId}}"
5
+ custom-ad="{{resolvedCustomAd}}"
6
+ ad-intervals="{{adIntervals}}"
7
+ bind:load="handleLoad"
8
+ bind:error="handleError"
9
+ bind:close="handleClose"
10
+ bind:action="handleAction"
11
+ bind:openWebview="handleOpenWebview"
12
+ />
13
+
14
+ <!-- 开屏广告组件 -->
15
+ <bi-splash-ad
16
+ wx:elif="{{resolvedType === 'splash'}}"
17
+ unit-id="{{resolvedUnitId}}"
18
+ custom-ad="{{resolvedCustomAd}}"
19
+ show="{{resolvedShow}}"
20
+ z-index="{{zIndex}}"
21
+ picture-in-picture="{{pictureInPicture}}"
22
+ check-native-splash="{{resolvedCheckNativeSplash}}"
23
+ jump-seconds="{{jumpSeconds}}"
24
+ bind:load="handleLoad"
25
+ bind:error="handleError"
26
+ bind:close="handleClose"
27
+ bind:action="handleAction"
28
+ bind:hide="handleHide"
29
+ bind:openWebview="handleOpenWebview"
30
+ />
@@ -0,0 +1 @@
1
+ /* bi-display-ad styles are delegated to bi-banner-ad and bi-splash-ad */