@bi-nova/ui-weapp 1.2.1 → 1.3.0-beta.202609181052

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,590 @@
1
+ /**
2
+ * @Description 产品投放位统一分发组件
3
+ * @Author wutao
4
+ * @Date 2026-09-08
5
+ * @LastEditors wutao
6
+ * @LastEditTime 2026-09-15 11:04:57
7
+ */
8
+ const { normalizeProductPlacementConfig } = require('./utils')
9
+ const { executeProductPlacementAction } = require('./action-executor')
10
+
11
+ Component({
12
+ properties: {
13
+ /** 标准化产品投放位配置 */
14
+ config: { type: Object, value: null },
15
+ /** 产品 UI 配置主键,也就是用于查询配置的广告 ID */
16
+ adId: { type: String, value: '' },
17
+ /** 调用方预期的展示形态,仅用于约束校验,不覆盖配置中的 displayMode */
18
+ type: { type: String, value: '' },
19
+ /** 是否允许展示当前投放位 */
20
+ show: { type: Boolean, value: true },
21
+ /** 业务动作是否正在执行 */
22
+ actionLoading: { type: Boolean, value: false },
23
+ /** 是否由组件自动执行标准动作 */
24
+ autoAction: { type: Boolean, value: true },
25
+ /** 动作执行所需的宿主运行时上下文 */
26
+ actionContext: { type: Object, value: null },
27
+ /** 弹层展示层级 */
28
+ zIndex: { type: Number, value: 999 },
29
+ /** 全局 Nova SDK 挂载字段名 */
30
+ sdkKey: { type: String, value: 'biNova' },
31
+ /** 未传本地 config 时是否按广告 ID 自动获取配置 */
32
+ autoLoad: { type: Boolean, value: true }
33
+ },
34
+ data: {
35
+ resolvedConfig: null,
36
+ resolvedDisplayMode: '',
37
+ resolvedCreative: null,
38
+ resolvedPanelStyle: null,
39
+ resolvedVisible: false,
40
+ resolvedActionable: false,
41
+ resolvedActionLoading: false,
42
+ actionExecuting: false,
43
+ remoteLoading: false,
44
+ remoteLoaded: false,
45
+ remoteConfig: null
46
+ },
47
+ lifetimes: {
48
+ created() {
49
+ /** 请求版本号必须在属性 observer 触发前初始化,避免 attached 阶段重置正在执行的请求 */
50
+ this.requestToken = 0
51
+ /** 动作版本号用于忽略配置切换或组件销毁后的异步回调 */
52
+ this.actionToken = 0
53
+ },
54
+ attached() {
55
+ this.displayCycle = 0
56
+ this.localDismissed = false
57
+ this.lastShow = Boolean(this.properties.show)
58
+ this.syncActionLoading()
59
+ this.syncConfig()
60
+ this.refreshRemoteConfig('attached')
61
+ },
62
+ detached() {
63
+ this.configIdentity = ''
64
+ this.loadedCycleKey = ''
65
+ this.exposedCycleKey = ''
66
+ this.requestToken = (this.requestToken || 0) + 1
67
+ this.actionToken = (this.actionToken || 0) + 1
68
+ }
69
+ },
70
+ observers: {
71
+ 'config, adId, type, sdkKey, autoLoad'() {
72
+ this.syncConfig()
73
+ this.refreshRemoteConfig('observer')
74
+ },
75
+ show(nextShow) {
76
+ const visible = Boolean(nextShow)
77
+
78
+ if (visible && this.lastShow === false) {
79
+ this.displayCycle = (this.displayCycle || 0) + 1
80
+ this.localDismissed = false
81
+ this.loadedCycleKey = ''
82
+ this.exposedCycleKey = ''
83
+ }
84
+
85
+ this.lastShow = visible
86
+ this.syncVisible()
87
+ },
88
+ actionLoading() {
89
+ this.syncActionLoading()
90
+ }
91
+ },
92
+ methods: {
93
+ /**
94
+ * 标准化并同步产品投放位配置
95
+ * @returns {void}
96
+ */
97
+ syncConfig() {
98
+ const result = normalizeProductPlacementConfig(this.getEffectiveConfig(), {
99
+ adId: this.properties.adId,
100
+ type: this.properties.type
101
+ })
102
+
103
+ if (!result.valid) {
104
+ this.actionToken = (this.actionToken || 0) + 1
105
+ this.configIdentity = ''
106
+ this.localDismissed = false
107
+ this.setActionExecuting(false)
108
+ this.setData({
109
+ resolvedConfig: null,
110
+ resolvedDisplayMode: '',
111
+ resolvedCreative: null,
112
+ resolvedPanelStyle: null,
113
+ resolvedVisible: false,
114
+ resolvedActionable: false
115
+ })
116
+
117
+ if (!result.silent) {
118
+ this.triggerConfigErrorOnce(result)
119
+ }
120
+
121
+ return
122
+ }
123
+
124
+ const normalizedConfig = result.config
125
+ const nextIdentity = this.createConfigIdentity(normalizedConfig)
126
+ const configChanged = nextIdentity !== this.configIdentity
127
+
128
+ if (configChanged) {
129
+ this.actionToken = (this.actionToken || 0) + 1
130
+ this.configIdentity = nextIdentity
131
+ this.displayCycle = (this.displayCycle || 0) + 1
132
+ this.localDismissed = false
133
+ this.loadedCycleKey = ''
134
+ this.exposedCycleKey = ''
135
+ this.lastErrorKey = ''
136
+ this.setActionExecuting(false)
137
+ }
138
+
139
+ this.setData({
140
+ resolvedConfig: normalizedConfig,
141
+ resolvedDisplayMode: normalizedConfig.displayMode,
142
+ resolvedCreative: normalizedConfig.creative,
143
+ resolvedPanelStyle: normalizedConfig.panelStyle,
144
+ resolvedVisible: Boolean(this.properties.show && !this.localDismissed),
145
+ resolvedActionable: normalizedConfig.actionable
146
+ }, () => {
147
+ if (configChanged) {
148
+ this.triggerEvent('configload', this.createEventDetail({
149
+ config: normalizedConfig
150
+ }))
151
+ }
152
+ })
153
+ },
154
+
155
+ /**
156
+ * 判断调用方是否显式传入了本地配置
157
+ * @returns {boolean}
158
+ */
159
+ hasLocalConfig() {
160
+ const config = this.properties.config
161
+ return Boolean(config && typeof config === 'object' && Object.keys(config).length > 0)
162
+ },
163
+
164
+ /**
165
+ * 获取当前优先使用的配置,本地配置优先于远程配置
166
+ * @returns {Record<string, any> | null}
167
+ */
168
+ getEffectiveConfig() {
169
+ return this.hasLocalConfig() ? this.properties.config : this.data.remoteConfig
170
+ },
171
+
172
+ /**
173
+ * 判断是否需要自动加载远程配置
174
+ * @returns {boolean}
175
+ */
176
+ shouldUseRemoteConfig() {
177
+ return Boolean(
178
+ this.properties.autoLoad &&
179
+ String(this.properties.adId || '').trim() &&
180
+ !this.hasLocalConfig()
181
+ )
182
+ },
183
+
184
+ /**
185
+ * 获取全局 Nova SDK 实例
186
+ * @returns {Record<string, any> | null}
187
+ */
188
+ getSdkInstance() {
189
+ const app = typeof getApp === 'function' ? getApp() : null
190
+
191
+ if (!app) return null
192
+
193
+ const sdkKey = this.properties.sdkKey || 'biNova'
194
+ return app[sdkKey] || app.globalData && app.globalData[sdkKey] || null
195
+ },
196
+
197
+ /**
198
+ * 构建远程配置请求参数
199
+ * @returns {Record<string, any>}
200
+ */
201
+ buildRemoteRequestOptions() {
202
+ const requestOptions = {
203
+ id: String(this.properties.adId || '').trim()
204
+ }
205
+
206
+ return requestOptions
207
+ },
208
+
209
+ /**
210
+ * 构建远程请求去重键
211
+ * @param {Record<string, any>} requestOptions 请求参数
212
+ * @returns {string}
213
+ */
214
+ createRemoteRequestKey(requestOptions) {
215
+ return JSON.stringify(requestOptions)
216
+ },
217
+
218
+ /**
219
+ * 触发远程配置错误事件
220
+ * @param {Record<string, any>} detail 错误详情
221
+ * @returns {void}
222
+ */
223
+ triggerRemoteError(detail) {
224
+ const eventDetail = this.createEventDetail(detail)
225
+ this.triggerEvent('configerror', eventDetail)
226
+ this.triggerEvent('error', eventDetail)
227
+ },
228
+
229
+ /**
230
+ * 触发无可展示配置的终态关闭事件
231
+ * @param {string} requestKey 请求标识
232
+ * @returns {void}
233
+ */
234
+ triggerNoConfigClose(requestKey) {
235
+ if (!this.properties.show || this.lastNoConfigKey === requestKey) return
236
+
237
+ this.lastNoConfigKey = requestKey
238
+ this.triggerEvent('close', this.createEventDetail({ reason: 'noConfig' }))
239
+ },
240
+
241
+ /**
242
+ * 按广告 ID 自动获取产品 UI 配置
243
+ * @param {string} reason 请求原因
244
+ * @param {boolean} [force] 是否强制刷新组件侧请求状态
245
+ * @returns {Promise<void>}
246
+ */
247
+ async refreshRemoteConfig(reason, force) {
248
+ if (!this.shouldUseRemoteConfig()) return
249
+
250
+ const requestOptions = this.buildRemoteRequestOptions()
251
+ const requestKey = this.createRemoteRequestKey(requestOptions)
252
+
253
+ if (
254
+ !force &&
255
+ this.lastRemoteRequestKey === requestKey &&
256
+ (this.data.remoteLoading || this.data.remoteLoaded)
257
+ ) {
258
+ return
259
+ }
260
+
261
+ const sdk = this.getSdkInstance()
262
+
263
+ if (!sdk || !sdk.ads || typeof sdk.ads.getProductConfig !== 'function') {
264
+ this.lastRemoteRequestKey = requestKey
265
+ this.setData({
266
+ remoteLoading: false,
267
+ remoteLoaded: true,
268
+ remoteConfig: null
269
+ })
270
+ this.syncConfig()
271
+ this.triggerRemoteError({
272
+ errCode: 'PRODUCT_PLACEMENT_SDK_NOT_READY',
273
+ errMsg: '未获取到产品 UI 配置服务,请升级并初始化 @bi-nova/services',
274
+ errorType: 'REMOTE_CONFIG_ERROR'
275
+ })
276
+ return
277
+ }
278
+
279
+ const requestToken = (this.requestToken || 0) + 1
280
+ this.requestToken = requestToken
281
+ this.lastRemoteRequestKey = requestKey
282
+ this.lastNoConfigKey = ''
283
+ this.setData({
284
+ remoteLoading: true,
285
+ remoteLoaded: false,
286
+ remoteConfig: null
287
+ })
288
+ this.syncConfig()
289
+
290
+ try {
291
+ const result = await sdk.ads.getProductConfig(requestOptions)
292
+ if (requestToken !== this.requestToken) return
293
+
294
+ if (!result || result.code !== 0 || !result.data) {
295
+ this.setData({
296
+ remoteLoading: false,
297
+ remoteLoaded: true,
298
+ remoteConfig: null
299
+ })
300
+ this.syncConfig()
301
+ this.triggerRemoteError({
302
+ errCode: 'PRODUCT_PLACEMENT_REMOTE_CONFIG_FAIL',
303
+ errMsg: result && result.message || '获取产品 UI 配置失败',
304
+ errorType: 'REMOTE_CONFIG_ERROR',
305
+ rawResult: result || null
306
+ })
307
+ return
308
+ }
309
+
310
+ const remoteConfig = result.data.show && result.data.config
311
+ ? result.data.config
312
+ : null
313
+
314
+ this.setData({
315
+ remoteLoading: false,
316
+ remoteLoaded: true,
317
+ remoteConfig
318
+ })
319
+ this.syncConfig()
320
+
321
+ if (!remoteConfig) {
322
+ this.triggerNoConfigClose(requestKey)
323
+ }
324
+ } catch (error) {
325
+ if (requestToken !== this.requestToken) return
326
+
327
+ this.setData({
328
+ remoteLoading: false,
329
+ remoteLoaded: true,
330
+ remoteConfig: null
331
+ })
332
+ this.syncConfig()
333
+ this.triggerRemoteError({
334
+ errCode: 'PRODUCT_PLACEMENT_REMOTE_CONFIG_EXCEPTION',
335
+ errMsg: error && error.message || '获取产品 UI 配置异常',
336
+ errorType: 'REMOTE_CONFIG_ERROR',
337
+ error
338
+ })
339
+ }
340
+ },
341
+
342
+ /**
343
+ * 供业务页面主动刷新远程产品配置
344
+ * @returns {Promise<void>}
345
+ */
346
+ refresh() {
347
+ return this.refreshRemoteConfig('manual', true)
348
+ },
349
+
350
+ /**
351
+ * 构建配置唯一标识
352
+ * @param {Record<string, any>} config 标准化配置
353
+ * @returns {string}
354
+ */
355
+ createConfigIdentity(config) {
356
+ return JSON.stringify(config)
357
+ },
358
+
359
+ /**
360
+ * 获取当前展示周期标识
361
+ * @returns {string}
362
+ */
363
+ getDisplayCycleKey() {
364
+ return `${this.configIdentity || 'empty'}__${this.displayCycle || 0}`
365
+ },
366
+
367
+ /**
368
+ * 同步最终展示状态
369
+ * @returns {void}
370
+ */
371
+ syncVisible() {
372
+ this.setData({
373
+ resolvedVisible: Boolean(
374
+ this.data.resolvedConfig &&
375
+ this.properties.show &&
376
+ !this.localDismissed
377
+ )
378
+ })
379
+ },
380
+
381
+ /**
382
+ * 同步外部与组件内部动作加载状态
383
+ * @returns {void}
384
+ */
385
+ syncActionLoading() {
386
+ this.setData({
387
+ resolvedActionLoading: Boolean(
388
+ this.properties.actionLoading || this.data.actionExecuting
389
+ )
390
+ })
391
+ },
392
+
393
+ /**
394
+ * 更新组件内部动作执行状态
395
+ * @param {boolean} executing 是否正在执行
396
+ * @returns {void}
397
+ */
398
+ setActionExecuting(executing) {
399
+ this.setData({
400
+ actionExecuting: Boolean(executing),
401
+ resolvedActionLoading: Boolean(
402
+ this.properties.actionLoading || executing
403
+ )
404
+ })
405
+ },
406
+
407
+ /**
408
+ * 创建统一事件上下文
409
+ * @param {Record<string, any>} [extra] 附加事件数据
410
+ * @returns {Record<string, any>}
411
+ */
412
+ createEventDetail(extra) {
413
+ const config = this.data.resolvedConfig || {}
414
+
415
+ return {
416
+ configId: config.configId,
417
+ adId: config.adId || this.properties.adId,
418
+ placementId: config.placementId || '',
419
+ productCode: config.productCode || '',
420
+ systemType: config.systemType || '',
421
+ providerCode: config.providerCode || '',
422
+ channelCode: config.channelCode || '',
423
+ sceneCode: config.sceneCode || '',
424
+ displayMode: config.displayMode || this.data.resolvedDisplayMode,
425
+ ...(extra || {})
426
+ }
427
+ },
428
+
429
+ /**
430
+ * 避免同一配置错误因属性观察器重复触发
431
+ * @param {{errCode: string, errMsg: string}} errorDetail 配置错误
432
+ * @returns {void}
433
+ */
434
+ triggerConfigErrorOnce(errorDetail) {
435
+ const errorKey = `${errorDetail.errCode}_${errorDetail.errMsg}`
436
+
437
+ if (this.lastErrorKey === errorKey) return
438
+
439
+ this.lastErrorKey = errorKey
440
+ this.triggerEvent('error', this.createEventDetail({
441
+ errCode: errorDetail.errCode,
442
+ errMsg: errorDetail.errMsg,
443
+ errorType: 'CONFIG_INVALID'
444
+ }))
445
+ },
446
+
447
+ /**
448
+ * 处理子组件完成渲染
449
+ * @param {Record<string, any>} event 子组件事件
450
+ * @returns {void}
451
+ */
452
+ handleReady(event) {
453
+ const cycleKey = this.getDisplayCycleKey()
454
+
455
+ if (this.loadedCycleKey !== cycleKey) {
456
+ this.loadedCycleKey = cycleKey
457
+ this.triggerEvent('load', this.createEventDetail(event && event.detail))
458
+ }
459
+
460
+ if (this.exposedCycleKey !== cycleKey && this.data.resolvedVisible) {
461
+ this.exposedCycleKey = cycleKey
462
+ this.triggerEvent('exposure', this.createEventDetail())
463
+ }
464
+ },
465
+
466
+ /**
467
+ * 处理素材加载失败并终止当前展示周期
468
+ * @param {Record<string, any>} event 子组件错误事件
469
+ * @returns {void}
470
+ */
471
+ handleError(event) {
472
+ const detail = event && event.detail || {}
473
+
474
+ this.triggerEvent('error', this.createEventDetail({
475
+ errCode: detail.errCode || 'PRODUCT_PLACEMENT_MEDIA_LOAD_FAIL',
476
+ errMsg: detail.errMsg || '产品投放位素材加载失败',
477
+ errorType: detail.errorType || 'MEDIA_LOAD_FAIL',
478
+ rawErrCode: detail.rawErrCode,
479
+ rawErrMsg: detail.rawErrMsg
480
+ }))
481
+ this.closePlacement('mediaLoadFail')
482
+ },
483
+
484
+ /**
485
+ * 处理用户关闭事件
486
+ * @param {Record<string, any>} event 子组件关闭事件
487
+ * @returns {void}
488
+ */
489
+ handleClose(event) {
490
+ const reason = event && event.detail && event.detail.reason || 'closeButton'
491
+ this.closePlacement(reason)
492
+ },
493
+
494
+ /**
495
+ * 关闭当前投放位并上报标准事件
496
+ * @param {string} reason 关闭原因
497
+ * @returns {void}
498
+ */
499
+ closePlacement(reason) {
500
+ if (this.localDismissed) return
501
+
502
+ this.localDismissed = true
503
+ this.syncVisible()
504
+ this.triggerEvent('close', this.createEventDetail({ reason }))
505
+ },
506
+
507
+ /**
508
+ * 处理产品主动作并根据标准动作配置完成分发
509
+ * @param {Record<string, any>} event 子组件动作事件
510
+ * @returns {void}
511
+ */
512
+ handleAction(event) {
513
+ if (this.data.resolvedActionLoading || !this.data.resolvedActionable) return
514
+
515
+ const detail = event && event.detail || {}
516
+ const source = detail.source || 'surface'
517
+ const action = this.data.resolvedConfig.action
518
+ const actionContext = this.properties.actionContext || {}
519
+ const actionDetail = this.createEventDetail({ source, action })
520
+
521
+ this.triggerEvent('action', actionDetail)
522
+
523
+ if (!this.properties.autoAction) return
524
+
525
+ const actionToken = (this.actionToken || 0) + 1
526
+ this.actionToken = actionToken
527
+ this.setActionExecuting(true)
528
+
529
+ const isCurrentAction = () => actionToken === this.actionToken
530
+ const completeAction = () => {
531
+ if (!isCurrentAction()) return false
532
+
533
+ this.setActionExecuting(false)
534
+ return true
535
+ }
536
+
537
+ try {
538
+ executeProductPlacementAction({
539
+ action,
540
+ sdk: this.getSdkInstance(),
541
+ actionContext,
542
+ apiRequestParams: {
543
+ configId: this.data.resolvedConfig.configId,
544
+ equipmentValue: actionContext.equipmentValue,
545
+ userId: actionContext.userId
546
+ },
547
+ onOpenWebview: (resultDetail) => {
548
+ if (!isCurrentAction()) return
549
+
550
+ this.triggerEvent('openWebview', this.createEventDetail({
551
+ source,
552
+ action,
553
+ url: resultDetail.url
554
+ }))
555
+ },
556
+ onSuccess: (resultDetail) => {
557
+ if (!completeAction()) return
558
+
559
+ this.triggerEvent('actionsuccess', this.createEventDetail({
560
+ source,
561
+ action,
562
+ ...resultDetail
563
+ }))
564
+ },
565
+ onError: (errorDetail) => {
566
+ if (!completeAction()) return
567
+
568
+ this.triggerEvent('error', this.createEventDetail({
569
+ source,
570
+ action,
571
+ errCode: errorDetail.errCode,
572
+ errMsg: errorDetail.errMsg,
573
+ errorType: 'ACTION_EXECUTE_FAIL'
574
+ }))
575
+ }
576
+ })
577
+ } catch (error) {
578
+ if (!completeAction()) return
579
+
580
+ this.triggerEvent('error', this.createEventDetail({
581
+ source,
582
+ action,
583
+ errCode: 'PRODUCT_PLACEMENT_ACTION_EXECUTE_EXCEPTION',
584
+ errMsg: error && error.message || '产品投放位动作执行异常',
585
+ errorType: 'ACTION_EXECUTE_FAIL'
586
+ }))
587
+ }
588
+ }
589
+ }
590
+ })
@@ -0,0 +1,8 @@
1
+ {
2
+ "component": true,
3
+ "usingComponents": {
4
+ "bi-product-banner": "./banner/index",
5
+ "bi-product-popup": "./popup/index",
6
+ "bi-product-half-screen": "./half-screen/index"
7
+ }
8
+ }
@@ -0,0 +1,44 @@
1
+ <!--
2
+ @Description 产品投放位统一分发组件模板
3
+ @Author wutao
4
+ @Date 2026-09-08
5
+ @LastEditors wutao
6
+ @LastEditTime 2026-09-14 17:19:39
7
+ -->
8
+ <bi-product-banner
9
+ wx:if="{{resolvedVisible && resolvedDisplayMode === 'banner'}}"
10
+ creative="{{resolvedCreative}}"
11
+ panel-style="{{resolvedPanelStyle}}"
12
+ actionable="{{resolvedActionable}}"
13
+ action-loading="{{resolvedActionLoading}}"
14
+ bind:ready="handleReady"
15
+ bind:error="handleError"
16
+ bind:close="handleClose"
17
+ bind:action="handleAction"
18
+ />
19
+
20
+ <bi-product-popup
21
+ wx:elif="{{resolvedVisible && resolvedDisplayMode === 'popup'}}"
22
+ creative="{{resolvedCreative}}"
23
+ panel-style="{{resolvedPanelStyle}}"
24
+ actionable="{{resolvedActionable}}"
25
+ action-loading="{{resolvedActionLoading}}"
26
+ z-index="{{zIndex}}"
27
+ bind:ready="handleReady"
28
+ bind:error="handleError"
29
+ bind:close="handleClose"
30
+ bind:action="handleAction"
31
+ />
32
+
33
+ <bi-product-half-screen
34
+ wx:elif="{{resolvedVisible && resolvedDisplayMode === 'half_screen'}}"
35
+ creative="{{resolvedCreative}}"
36
+ panel-style="{{resolvedPanelStyle}}"
37
+ actionable="{{resolvedActionable}}"
38
+ action-loading="{{resolvedActionLoading}}"
39
+ z-index="{{zIndex}}"
40
+ bind:ready="handleReady"
41
+ bind:error="handleError"
42
+ bind:close="handleClose"
43
+ bind:action="handleAction"
44
+ />
@@ -0,0 +1,11 @@
1
+ /*
2
+ * @Description 产品投放位统一分发组件样式
3
+ * @Author wutao
4
+ * @Date 2026-09-08
5
+ * @LastEditors wutao
6
+ * @LastEditTime 2026-09-11 18:18:33
7
+ */
8
+ :host {
9
+ display: block;
10
+ width: 100%;
11
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @Description 产品投放位通用遮罩组件
3
+ * @Author wutao
4
+ * @Date 2026-09-08
5
+ * @LastEditors wutao
6
+ * @LastEditTime 2026-09-08 17:55:14
7
+ */
8
+ Component({
9
+ options: {
10
+ multipleSlots: true
11
+ },
12
+ properties: {
13
+ show: { type: Boolean, value: true },
14
+ position: { type: String, value: 'center' },
15
+ maskClosable: { type: Boolean, value: true },
16
+ zIndex: { type: Number, value: 999 }
17
+ },
18
+ methods: {
19
+ /**
20
+ * 处理遮罩点击,仅在允许时通知上层关闭
21
+ * @returns {void}
22
+ */
23
+ handleMaskTap() {
24
+ if (!this.properties.maskClosable) return
25
+
26
+ this.triggerEvent('maskclose', { reason: 'mask' })
27
+ },
28
+
29
+ /** 阻止内容点击和触摸事件穿透至遮罩或页面 */
30
+ stopPropagation() {}
31
+ }
32
+ })
@@ -0,0 +1,3 @@
1
+ {
2
+ "component": true
3
+ }
@@ -0,0 +1,18 @@
1
+ <!--
2
+ @Description 产品投放位通用遮罩组件模板
3
+ @Author wutao
4
+ @Date 2026-09-08
5
+ @LastEditors wutao
6
+ @LastEditTime 2026-09-08 17:55:14
7
+ -->
8
+ <view
9
+ wx:if="{{show}}"
10
+ class="bi-product-overlay bi-product-overlay--{{position}}"
11
+ style="z-index: {{zIndex}};"
12
+ catchtouchmove="stopPropagation"
13
+ >
14
+ <view class="bi-product-overlay__mask" bindtap="handleMaskTap"></view>
15
+ <view class="bi-product-overlay__content" catchtap="stopPropagation">
16
+ <slot></slot>
17
+ </view>
18
+ </view>