@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.
- package/package.json +1 -1
- package/src/ad/utils.js +3 -2
- package/src/product-placement/README.md +607 -0
- package/src/product-placement/action-executor.js +293 -0
- package/src/product-placement/banner/index.js +106 -0
- package/src/product-placement/banner/index.json +3 -0
- package/src/product-placement/banner/index.wxml +48 -0
- package/src/product-placement/banner/index.wxss +133 -0
- package/src/product-placement/half-screen/index.js +129 -0
- package/src/product-placement/half-screen/index.json +6 -0
- package/src/product-placement/half-screen/index.wxml +64 -0
- package/src/product-placement/half-screen/index.wxss +145 -0
- package/src/product-placement/index.js +590 -0
- package/src/product-placement/index.json +8 -0
- package/src/product-placement/index.wxml +44 -0
- package/src/product-placement/index.wxss +11 -0
- package/src/product-placement/overlay/index.js +32 -0
- package/src/product-placement/overlay/index.json +3 -0
- package/src/product-placement/overlay/index.wxml +18 -0
- package/src/product-placement/overlay/index.wxss +55 -0
- package/src/product-placement/popup/index.js +112 -0
- package/src/product-placement/popup/index.json +6 -0
- package/src/product-placement/popup/index.wxml +54 -0
- package/src/product-placement/popup/index.wxss +124 -0
- package/src/product-placement/utils.js +295 -0
- package/src/shared/action-executor.js +336 -0
|
@@ -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,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,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,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>
|