@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bi-nova/ui-weapp",
3
- "version": "1.2.1",
3
+ "version": "1.3.0-beta.202609181052",
4
4
  "description": "BI Nova SDK WeChat Mini Program Components",
5
5
  "miniprogram": "src",
6
6
  "license": "ISC",
package/src/ad/utils.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * @Author wutao
4
4
  * @Date 2026-04-17
5
5
  * @LastEditors wutao
6
- * @LastEditTime 2026-09-07 17:00:17
6
+ * @LastEditTime 2026-09-17 18:05:17
7
7
  */
8
8
 
9
9
  /**
@@ -144,7 +144,8 @@ function handleApiResponse(response, context) {
144
144
  action: {
145
145
  type: 'miniProgram',
146
146
  appId,
147
- path
147
+ path,
148
+ miniProgramOpenMode: 'half'
148
149
  }
149
150
  }
150
151
  })
@@ -0,0 +1,607 @@
1
+ # 微信小程序产品投放位组件
2
+
3
+ `bi-product-placement` 是面向微信小程序的产品投放位统一组件。接入方只需传入我方分配的 `ad-id`、预期展示形态和当前业务所需的运行时参数,组件会自动完成远程配置获取、配置校验、UI 渲染、曝光控制以及点击动作执行。
4
+
5
+ ## 1. 组件能力
6
+
7
+ | 能力 | 说明 |
8
+ | --- | --- |
9
+ | 统一展示入口 | 根据远程配置渲染页面内 Banner、居中弹窗或底部半屏。 |
10
+ | 自动配置获取 | 通过全局 Nova SDK 的 `ads.getProductConfig()` 按 `ad-id` 获取配置。 |
11
+ | 配置安全校验 | 校验广告 ID、展示形态、系统类型、素材、动作类型和皮肤白名单。 |
12
+ | 标准动作执行 | 支持跳转其他小程序、跳转当前小程序页面、打开 H5、动态接口动作和内置产品业务动作。 |
13
+ | 统一生命周期事件 | 提供配置加载、内容就绪、曝光、点击、动作成功、关闭和错误事件。 |
14
+ | 防重复操作 | 自动动作执行期间进入加载态并阻止重复点击。 |
15
+
16
+ ## 2. 接入前准备
17
+
18
+ 接入前请向我方确认以下信息:
19
+
20
+ 1. 当前环境可用的 `ad-id`。
21
+ 2. 该 `ad-id` 对应的展示形态:`banner`、`popup` 或 `half_screen`。
22
+ 3. 配置的动作类型,以及该动作要求的 `action-context` 参数。
23
+ 4. 如动作可能打开 H5,确认接入方已有 WebView 承载页,并已配置对应业务域名。
24
+
25
+ ### 2.1 安装依赖
26
+
27
+ ```bash
28
+ npm install @bi-nova/core @bi-nova/services @bi-nova/ui-weapp
29
+ ```
30
+
31
+ 也可以使用项目现有的包管理器:
32
+
33
+ ```bash
34
+ pnpm add @bi-nova/core @bi-nova/services @bi-nova/ui-weapp
35
+ ```
36
+
37
+ ```bash
38
+ yarn add @bi-nova/core @bi-nova/services @bi-nova/ui-weapp
39
+ ```
40
+
41
+ 安装完成后,在微信开发者工具中执行“工具 -> 构建 npm”。最终组件路径以项目生成的 `miniprogram_npm` 目录为准。
42
+
43
+ ### 2.2 配置小程序合法域名
44
+
45
+ 请在微信公众平台将以下地址配置为小程序的 `request 合法域名`:
46
+
47
+ ```text
48
+ https://nad.ehuoke.com
49
+ ```
50
+
51
+ 未配置时,产品投放配置请求会被微信拦截,组件将触发 `configerror` 和 `error` 事件。
52
+
53
+ 如果产品动作会打开 H5,还需要按微信 WebView 规则配置目标页面的业务域名;该域名取决于实际投放配置,不一定与配置接口域名相同。
54
+
55
+ 如果产品配置使用 `api` 动作,还需要将动作 URL 所属域名加入小程序的 `request 合法域名`。具体域名请以我方提供的投放配置为准。
56
+
57
+ ## 3. 初始化 Nova SDK
58
+
59
+ 在 `app.js` 中创建 Nova SDK,注册 `@bi-nova/services`,并将实例挂载到 `App` 实例或 `globalData`。推荐使用 `globalData.biNova`:
60
+
61
+ ```js
62
+ const { createNova } = require('@bi-nova/core')
63
+ const { createServices } = require('@bi-nova/services')
64
+
65
+ const biNova = createNova({
66
+ appId: 'your-app-id',
67
+ autoEnv: true
68
+ })
69
+
70
+ biNova.use(createServices({
71
+ env: 'prod',
72
+ debug: false
73
+ }))
74
+
75
+ App({
76
+ globalData: {
77
+ biNova
78
+ }
79
+ })
80
+ ```
81
+
82
+ 说明:
83
+
84
+ - 微信小程序环境需要设置 `autoEnv: true`。
85
+ - `createNova` 的 `appId` 用于 SDK 初始化;产品投放配置仍以组件的 `ad-id` 为查询条件。
86
+ - 仅获取产品投放配置不要求登录 token。若对应的产品业务服务需要登录态,请在用户登录后通过同一个 SDK 实例调用 `setToken()`。
87
+ - 组件默认读取 `app.biNova` 或 `app.globalData.biNova`。如挂载字段不是 `biNova`,需通过组件的 `sdk-key` 指定。
88
+
89
+ 自定义 SDK 挂载字段示例:
90
+
91
+ ```xml
92
+ <bi-product-placement
93
+ ad-id="product_placement_xxx"
94
+ type="banner"
95
+ sdk-key="novaSdk"
96
+ />
97
+ ```
98
+
99
+ ## 4. 注册组件
100
+
101
+ 在页面或组件的 JSON 配置中注册:
102
+
103
+ ```json
104
+ {
105
+ "usingComponents": {
106
+ "bi-product-placement": "/miniprogram_npm/@bi-nova/ui-weapp/product-placement/index"
107
+ }
108
+ }
109
+ ```
110
+
111
+ 如果项目采用 monorepo 直接引用或源码复制方式,请按实际目录调整路径。
112
+
113
+ ## 5. 标准接入
114
+
115
+ ### 5.1 页面模板
116
+
117
+ ```xml
118
+ <bi-product-placement
119
+ id="productPlacement"
120
+ ad-id="{{productPlacementAdId}}"
121
+ type="half_screen"
122
+ show="{{showProductPlacement}}"
123
+ action-context="{{productPlacementActionContext}}"
124
+ bind:configload="handleProductPlacementConfigLoad"
125
+ bind:configerror="handleProductPlacementConfigError"
126
+ bind:load="handleProductPlacementLoad"
127
+ bind:exposure="handleProductPlacementExposure"
128
+ bind:action="handleProductPlacementAction"
129
+ bind:actionsuccess="handleProductPlacementActionSuccess"
130
+ bind:openWebview="handleProductPlacementOpenWebview"
131
+ bind:close="handleProductPlacementClose"
132
+ bind:error="handleProductPlacementError"
133
+ />
134
+ ```
135
+
136
+ ### 5.2 页面逻辑
137
+
138
+ ```js
139
+ Page({
140
+ data: {
141
+ // 请替换为我方分配的真实广告 ID。
142
+ productPlacementAdId: 'product_placement_xxx',
143
+ showProductPlacement: true,
144
+ productPlacementActionContext: {
145
+ // 下列字段仅作示例,请按“动作参数”章节传入当前动作需要的字段。
146
+ equipmentValue: 'device-id',
147
+ userId: 10001,
148
+ phone: '13800138000',
149
+ entrance: 'pay_success'
150
+ }
151
+ },
152
+
153
+ handleProductPlacementConfigLoad(event) {
154
+ console.log('产品投放配置可用', event.detail)
155
+ },
156
+
157
+ handleProductPlacementConfigError(event) {
158
+ console.error('产品投放配置加载失败', event.detail)
159
+ },
160
+
161
+ handleProductPlacementLoad(event) {
162
+ console.log('产品投放内容就绪', event.detail)
163
+ },
164
+
165
+ handleProductPlacementExposure(event) {
166
+ console.log('产品投放位曝光', event.detail)
167
+ },
168
+
169
+ handleProductPlacementAction(event) {
170
+ // action 事件表示用户已点击,可在此进行业务埋点。
171
+ console.log('产品投放位点击', event.detail)
172
+ },
173
+
174
+ handleProductPlacementActionSuccess(event) {
175
+ console.log('产品投放动作完成', event.detail)
176
+ },
177
+
178
+ handleProductPlacementOpenWebview(event) {
179
+ const url = encodeURIComponent(event.detail.url)
180
+ wx.navigateTo({
181
+ url: `/pages/webview/index?url=${url}`
182
+ })
183
+ },
184
+
185
+ handleProductPlacementClose(event) {
186
+ this.setData({ showProductPlacement: false })
187
+ console.log('产品投放位关闭', event.detail)
188
+ },
189
+
190
+ handleProductPlacementError(event) {
191
+ console.error('产品投放位异常', event.detail)
192
+ }
193
+ })
194
+ ```
195
+
196
+ `/pages/webview/index` 是示例路径,接入方需要替换为项目实际存在的 WebView 页面。
197
+
198
+ ## 6. 组件属性
199
+
200
+ | 属性 | 类型 | 默认值 | 必填 | 说明 |
201
+ | --- | --- | --- | :---: | --- |
202
+ | `ad-id` | `string` | `''` | 是 | 我方分配的广告 ID,也是远程配置的唯一业务查询参数。建议始终按字符串传入。 |
203
+ | `type` | `'banner' \| 'popup' \| 'half_screen'` | `''` | 建议 | 调用方预期的展示形态,仅用于安全校验,不会覆盖远程配置中的 `displayMode`。 |
204
+ | `show` | `boolean` | `true` | 否 | 是否允许展示当前投放位。 |
205
+ | `action-context` | `object` | `null` | 按动作 | 动作执行所需的宿主运行时参数,详见“动作参数”。 |
206
+ | `auto-action` | `boolean` | `true` | 否 | 是否由组件自动执行标准动作。设为 `false` 时组件只触发 `action`。 |
207
+ | `action-loading` | `boolean` | `false` | 否 | 外部动作加载状态。为 `true` 时按钮展示加载态并禁止重复点击。 |
208
+ | `z-index` | `number` | `999` | 否 | `popup` 和 `half_screen` 遮罩层的层级。 |
209
+ | `sdk-key` | `string` | `'biNova'` | 否 | Nova SDK 在 `App` 或 `globalData` 上的挂载字段名。 |
210
+ | `auto-load` | `boolean` | `true` | 否 | 未传本地 `config` 时,是否自动获取远程配置。标准接入请保持 `true`。 |
211
+ | `config` | `object` | `null` | 否 | 本地标准化配置。传入非空对象后优先使用本地配置且不再请求远程配置,仅建议联调使用。 |
212
+
213
+ ### 6.1 `show` 与重新展示
214
+
215
+ 用户点击关闭按钮、点击可关闭遮罩或遇到素材加载失败后,组件会立即在内部隐藏当前投放位。若需要再次展示,接入方必须让 `show` 先变为 `false`,再变为 `true`:
216
+
217
+ ```js
218
+ this.setData({ showProductPlacement: false }, () => {
219
+ this.setData({ showProductPlacement: true })
220
+ })
221
+ ```
222
+
223
+ 每次 `false -> true` 会开启新的展示周期,`load` 和 `exposure` 可在新周期中再次触发。
224
+
225
+ ## 7. 展示形态
226
+
227
+ | `type` / `displayMode` | 展示方式 | 关闭能力 |
228
+ | --- | --- | --- |
229
+ | `banner` | 页面文档流内的 Banner,组件宽度占满父容器。 | `panelStyle.closeable` 为 `true` 时展示关闭按钮。 |
230
+ | `popup` | 带遮罩的居中弹窗。 | 支持关闭按钮和点击遮罩关闭。 |
231
+ | `half_screen` | 带遮罩的底部半屏,内容区域可滚动。 | 支持关闭按钮和点击遮罩关闭。 |
232
+
233
+ 建议接入方始终传入 `type`。当 `type` 与远程配置的 `displayMode` 不一致时,组件不会展示,并触发:
234
+
235
+ ```text
236
+ PRODUCT_PLACEMENT_DISPLAY_MODE_MISMATCH
237
+ ```
238
+
239
+ 该校验可以避免后台配置错误导致页面布局从 Banner 意外变成弹层。
240
+
241
+ 组件内置 `default`、`vip_gold`、`insurance_blue` 三种皮肤。未知皮肤会降级为 `default`,远程配置不能注入任意 CSS 类名。
242
+
243
+ 半屏高度由配置的 `heightRatio` 决定,有效范围为 `0.4` 至 `0.85`,默认值为 `0.68`。
244
+
245
+ ## 8. 配置加载与展示生命周期
246
+
247
+ 标准接入时,组件按以下顺序工作:
248
+
249
+ 1. 使用 `ad-id` 调用 `biNova.ads.getProductConfig({ id })`。
250
+ 2. services 请求 `GET /gw/advert-plugin/api/v1/product-config/placement/{id}`,并标准化服务端配置。
251
+ 3. services 从接口 `body` 数组或兼容的 `data` 对象中,选择 `id` 或 `placementId` 与查询 ID 匹配的有效配置。
252
+ 4. 组件再次校验广告 ID、`type`、展示形态、系统类型和素材。
253
+ 5. 校验成功后触发 `configload` 并渲染对应展示组件。
254
+ 6. 图片加载成功或纯文案渲染就绪后先触发 `load`;当前处于显示状态时,随后触发 `exposure`。
255
+
256
+ 同一配置、同一展示周期内,`load` 和 `exposure` 均只触发一次。
257
+
258
+ 服务端没有匹配配置、配置已停用或配置被删除时,组件不展示内容,并触发 `close`,其 `detail.reason` 为 `noConfig`。这是正常的无投放终态,不属于请求异常。
259
+
260
+ ### 8.1 主动刷新配置
261
+
262
+ ```js
263
+ async refreshProductPlacement() {
264
+ const component = this.selectComponent('#productPlacement')
265
+ await component.refresh()
266
+ }
267
+ ```
268
+
269
+ `refresh()` 仅在 `auto-load="{{true}}"`、已传 `ad-id` 且未传本地 `config` 时有效。它会重置组件侧请求状态并重新调用 services;services 对同一 `ad-id` 有 30 秒内存缓存,因此 30 秒内刷新可能仍返回缓存结果。
270
+
271
+ ## 9. 点击动作
272
+
273
+ 默认 `auto-action="{{true}}"`,组件先触发 `action` 事件,再自动执行标准动作。动作完成后触发 `actionsuccess`;动作失败则触发 `error`。
274
+
275
+ | 动作类型 | 服务端动作字段 | 组件行为 | `action-context` 要求 |
276
+ | --- | --- | --- | --- |
277
+ | `mini_program` | `appId`,可选 `path` | 调用 `wx.navigateToMiniProgram`。 | 无 |
278
+ | `internal` | `path` | 调用 `wx.navigateTo` 打开当前小程序页面。 | 无 |
279
+ | `web` | `url` | 触发 `openWebview`,由接入方打开 H5;随后触发 `actionsuccess`。 | 无 |
280
+ | `api` | `url` | 发起 GET 请求,成功取得目标小程序信息后自动跳转。 | `equipmentValue`、正整数 `userId` 必填;`configId` 由组件自动补充。 |
281
+ | `business` | `businessCode` | 调用组件内置白名单业务能力。 | 由 `businessCode` 决定,见下文。 |
282
+ | `none` | 无 | 不产生可点击动作。 | 无 |
283
+
284
+ 服务端如返回 `miniProgram` 或 `miniprogram`,标准化后统一为 `mini_program`。
285
+
286
+ ### 9.1 `api` 动作
287
+
288
+ 组件会把以下参数追加到配置的动作 URL:
289
+
290
+ ```js
291
+ {
292
+ configId: '由当前产品配置自动提供',
293
+ equipmentValue: '由 action-context 提供',
294
+ userId: 10001
295
+ }
296
+ ```
297
+
298
+ 接口响应需要满足:
299
+
300
+ ```js
301
+ {
302
+ code: '200',
303
+ message: '成功',
304
+ body: {
305
+ appId: '目标小程序 AppID',
306
+ path: '目标页面路径'
307
+ }
308
+ }
309
+ ```
310
+
311
+ `body.url` 可兼容作为 `path` 使用。目标路径会移除开头的 `/` 后再传给 `wx.navigateToMiniProgram`。
312
+
313
+ ### 9.2 京东保险内部服务
314
+
315
+ 远程动作配置的 `businessCode` 为 `jd_insurance` 时,接入方通过 `action-context` 传入:
316
+
317
+ | 字段 | 类型 | 必填 | 说明 |
318
+ | --- | --- | :---: | --- |
319
+ | `equipmentValue` | `string` | 是 | 设备唯一标识。 |
320
+ | `phone` | `string` | 否 | 用户手机号。 |
321
+ | `entrance` | `string` | 否 | 当前业务入口标识。 |
322
+
323
+ ```xml
324
+ <bi-product-placement
325
+ ad-id="{{productPlacementAdId}}"
326
+ type="banner"
327
+ action-context="{{jdInsuranceActionContext}}"
328
+ />
329
+ ```
330
+
331
+ ```js
332
+ Page({
333
+ data: {
334
+ jdInsuranceActionContext: {
335
+ equipmentValue: '77013434',
336
+ phone: '13800138000',
337
+ entrance: 'pay_success'
338
+ }
339
+ }
340
+ })
341
+ ```
342
+
343
+ 组件会调用 `sdk.jd.handleJdInsurance()`。服务返回目标小程序时自动跳转;口令复制成功时展示成功提示;返回 H5 时触发 `openWebview`。
344
+
345
+ ### 9.3 京东保险外部对接服务
346
+
347
+ 远程动作配置的 `businessCode` 为 `jd_insurance_ext` 时,接入方通过 `action-context` 传入:
348
+
349
+ | 字段 | 类型 | 必填 | 说明 |
350
+ | --- | --- | :---: | --- |
351
+ | `thirdPlatform` | `string` | 是 | 第三方平台标识。 |
352
+ | `thirdPlatformUserId` | `string` | 是 | 第三方平台用户 ID。 |
353
+ | `thirdPlatformEquipmentValue` | `string` | 是 | 第三方平台设备标识。 |
354
+ | `phone` | `string` | 否 | 用户手机号。 |
355
+ | `extendData` | `object` | 否 | 外部服务协议允许的扩展数据。 |
356
+
357
+ ```js
358
+ Page({
359
+ data: {
360
+ jdInsuranceExtActionContext: {
361
+ thirdPlatform: 'yipule',
362
+ thirdPlatformUserId: '23413494',
363
+ thirdPlatformEquipmentValue: '999999',
364
+ phone: '13800138000',
365
+ extendData: {
366
+ source: 'product_placement'
367
+ }
368
+ }
369
+ }
370
+ })
371
+ ```
372
+
373
+ 组件会调用 `sdk.jd.handleJdInsuranceExt()`,并按与内部服务相同的方式处理小程序跳转、口令复制和 H5 结果。
374
+
375
+ ### 9.4 由接入方自行执行动作
376
+
377
+ 如需自行接管点击动作,请关闭自动执行,并在业务完成前维护 `action-loading`:
378
+
379
+ ```xml
380
+ <bi-product-placement
381
+ ad-id="{{productPlacementAdId}}"
382
+ type="banner"
383
+ auto-action="{{false}}"
384
+ action-loading="{{actionLoading}}"
385
+ bind:action="handleProductPlacementAction"
386
+ />
387
+ ```
388
+
389
+ ```js
390
+ Page({
391
+ data: {
392
+ actionLoading: false
393
+ },
394
+
395
+ async handleProductPlacementAction(event) {
396
+ this.setData({ actionLoading: true })
397
+
398
+ try {
399
+ await this.executeBusinessAction(event.detail.action)
400
+ } finally {
401
+ this.setData({ actionLoading: false })
402
+ }
403
+ }
404
+ })
405
+ ```
406
+
407
+ ## 10. 组件事件
408
+
409
+ 所有事件的 `event.detail` 都包含以下公共上下文:
410
+
411
+ | 字段 | 类型 | 说明 |
412
+ | --- | --- | --- |
413
+ | `adId` | `string` | 当前广告 ID。 |
414
+ | `configId` | `string \| number \| undefined` | 服务端配置主键;远程配置尚未加载成功时可能为空。 |
415
+ | `placementId` | `string` | 投放位业务标识。 |
416
+ | `productCode` | `string` | 产品编码。 |
417
+ | `systemType` | `string` | 标准化后的系统类型:`lyy` 或 `third_party`。 |
418
+ | `providerCode` | `string` | 产品提供方编码。 |
419
+ | `channelCode` | `string` | 渠道编码。 |
420
+ | `sceneCode` | `string` | 场景编码。 |
421
+ | `displayMode` | `string` | 实际展示形态。 |
422
+
423
+ 不同事件的附加字段如下:
424
+
425
+ | 事件 | 触发时机 | 主要附加字段 |
426
+ | --- | --- | --- |
427
+ | `configload` | 配置完成标准化和校验。 | `config`:完整标准化配置。 |
428
+ | `configerror` | SDK 未就绪、远程请求失败或请求异常。 | `errCode`、`errMsg`、`errorType`;同一错误也会触发 `error`。 |
429
+ | `load` | 图片加载成功或纯文案内容渲染就绪,表示内容已经具备展示条件。 | `mediaType`:`image` 或 `content`。 |
430
+ | `exposure` | 内容就绪且组件处于显示状态,表示组件已开始本次展示周期。 | 无。 |
431
+ | `action` | 用户点击可操作区域。 | `source`:`surface` 或 `button`;`action`:标准化动作。 |
432
+ | `actionsuccess` | 自动动作成功完成。 | `source`、`action`、`actionType`、`resultType`,以及动作对应的目标字段。 |
433
+ | `openWebview` | `web` 动作或产品业务返回 H5。 | `source`、`action`、`url`。 |
434
+ | `close` | 用户关闭、素材失败或没有可投放配置。 | `reason`,见下表。 |
435
+ | `error` | 配置、远程请求、素材或动作执行异常。 | `errCode`、`errMsg`、`errorType`。 |
436
+
437
+ ### 10.1 `load` 与 `exposure` 的区别
438
+
439
+ | 对比项 | `load` | `exposure` |
440
+ | --- | --- | --- |
441
+ | 核心含义 | 投放内容已准备完成。 | 准备完成的投放内容已进入组件展示状态。 |
442
+ | 触发条件 | 图片加载成功,或纯文案渲染就绪。 | 已满足 `load` 条件,并且 `show` 为 `true`、当前投放位未被关闭。 |
443
+ | 常见用途 | 取消页面加载状态、统计素材加载成功率、排查图片加载问题。 | 上报投放曝光、统计展示次数。 |
444
+ | 事件附加字段 | `mediaType`,值为 `image` 或 `content`。 | 无。 |
445
+ | 单周期次数 | 最多一次。 | 最多一次。 |
446
+
447
+ 正常展示时,两个事件通常连续触发,顺序为:
448
+
449
+ ```text
450
+ configload -> load -> exposure
451
+ ```
452
+
453
+ `exposure` 是组件级展示事件,不是严格的视口可见事件。它不会判断 Banner 是否已经滚动进入手机屏幕可视区域。例如 Banner 位于长页面底部时,素材完成加载后仍可能触发 `exposure`。
454
+
455
+ - `popup`、`half_screen` 属于立即覆盖页面的弹层,通常可以直接使用 `exposure` 作为曝光埋点。
456
+ - `banner` 如要求“进入可视区域且达到指定可见比例”才算曝光,应由接入方结合微信小程序 `IntersectionObserver` 二次判断,不要直接把组件的 `exposure` 当作严格可视曝光。
457
+
458
+ ### 10.2 关闭原因
459
+
460
+ `close.detail.reason` 取值:
461
+
462
+ | 值 | 说明 |
463
+ | --- | --- |
464
+ | `closeButton` | 用户点击关闭按钮。 |
465
+ | `mask` | 用户点击遮罩关闭弹层。 |
466
+ | `mediaLoadFail` | 展示图片加载失败,组件已终止当前展示周期。 |
467
+ | `noConfig` | 服务端没有当前 `ad-id` 对应的可展示配置。 |
468
+
469
+ ## 11. 本地配置联调
470
+
471
+ 本地配置仅用于开发联调或服务端配置尚未就绪的场景。生产环境建议始终使用 `ad-id` 自动获取配置。
472
+
473
+ ```xml
474
+ <bi-product-placement
475
+ ad-id="{{localProductPlacementConfig.configId}}"
476
+ type="half_screen"
477
+ auto-load="{{false}}"
478
+ config="{{localProductPlacementConfig}}"
479
+ action-context="{{jdInsuranceActionContext}}"
480
+ bind:action="handleProductPlacementAction"
481
+ bind:error="handleProductPlacementError"
482
+ />
483
+ ```
484
+
485
+ ```js
486
+ Page({
487
+ data: {
488
+ localProductPlacementConfig: {
489
+ schemaVersion: 2,
490
+ configId: 10001,
491
+ placementId: 'jd_insurance_pay_success',
492
+ productCode: 'jd_insurance',
493
+ systemType: 'third_party',
494
+ providerCode: 'jd',
495
+ channelCode: 'weapp',
496
+ sceneCode: 'pay_success',
497
+ displayMode: 'half_screen',
498
+ creative: {
499
+ imageUrl: 'https://example.com/jd-insurance.png',
500
+ title: '洗车即享保险权益',
501
+ description: '点击查看可领取的保障权益',
502
+ buttonText: '立即查看',
503
+ badgeText: '保险权益'
504
+ },
505
+ panelStyle: {
506
+ skinCode: 'insurance_blue',
507
+ heightRatio: 0.68,
508
+ closeable: true,
509
+ maskClosable: true
510
+ },
511
+ action: {
512
+ type: 'business',
513
+ businessCode: 'jd_insurance',
514
+ params: {}
515
+ }
516
+ }
517
+ }
518
+ })
519
+ ```
520
+
521
+ 本地配置约束:
522
+
523
+ - `displayMode` 只支持 `banner`、`popup`、`half_screen`。
524
+ - `systemType` 只支持 `lyy`、`third_party`。
525
+ - `creative` 至少需要 `imageUrl`、`title`、`description`、`buttonText` 中的一项。
526
+ - `panelStyle.closeable` 只有显式传入 `true` 才显示关闭按钮。
527
+ - `panelStyle.maskClosable` 默认是 `true`,显式传入 `false` 可禁止点击遮罩关闭。
528
+ - `panelStyle.showAgreement` 当前为标准化保留字段,组件暂不渲染协议区域。
529
+ - `action.params` 只保留字符串、数字和布尔值;宿主运行时参数必须通过 `action-context` 传入。
530
+ - `ad-id` 必须与本地配置的 `adId` 或 `configId` 一致,否则组件拒绝展示。
531
+
532
+ ## 12. 错误处理
533
+
534
+ `error.detail.errorType` 用于区分错误阶段:
535
+
536
+ | `errorType` | 说明 |
537
+ | --- | --- |
538
+ | `REMOTE_CONFIG_ERROR` | SDK 未初始化、远程配置请求失败或请求异常。 |
539
+ | `CONFIG_INVALID` | 返回配置或本地配置未通过组件校验。 |
540
+ | `MEDIA_LOAD_FAIL` | 展示图片加载失败。 |
541
+ | `ACTION_EXECUTE_FAIL` | 点击动作执行失败。 |
542
+
543
+ ### 12.1 配置与远程请求错误码
544
+
545
+ | 错误码 | 说明 |
546
+ | --- | --- |
547
+ | `PRODUCT_PLACEMENT_SDK_NOT_READY` | 未找到 `ads.getProductConfig()`,通常是 SDK 未挂载、services 未注册或版本不兼容。 |
548
+ | `PRODUCT_PLACEMENT_REMOTE_CONFIG_FAIL` | services 返回失败结果。 |
549
+ | `PRODUCT_PLACEMENT_REMOTE_CONFIG_EXCEPTION` | 配置请求执行期间发生异常。 |
550
+ | `PRODUCT_PLACEMENT_UNSUPPORTED_DISPLAY_MODE` | 配置中的展示形态不受支持。 |
551
+ | `PRODUCT_PLACEMENT_UNSUPPORTED_TYPE` | 组件传入的 `type` 不受支持。 |
552
+ | `PRODUCT_PLACEMENT_DISPLAY_MODE_MISMATCH` | 组件 `type` 与配置 `displayMode` 不一致。 |
553
+ | `PRODUCT_CONFIG_ID_MISMATCH` | `ad-id` 与返回配置不匹配。 |
554
+ | `PRODUCT_PLACEMENT_INVALID_SYSTEM_TYPE` | 配置中的系统类型不受支持。 |
555
+ | `PRODUCT_PLACEMENT_EMPTY_CREATIVE` | 配置没有任何可展示素材或文案。 |
556
+
557
+ 空配置在首次挂载和远程请求过程中会被静默处理;服务端明确返回无配置时,以 `close.reason = noConfig` 通知接入方。
558
+
559
+ ### 12.2 素材错误码
560
+
561
+ | 错误码 | 说明 |
562
+ | --- | --- |
563
+ | `PRODUCT_PLACEMENT_BANNER_IMAGE_LOAD_FAIL` | Banner 图片加载失败。 |
564
+ | `PRODUCT_PLACEMENT_POPUP_IMAGE_LOAD_FAIL` | 居中弹窗图片加载失败。 |
565
+ | `PRODUCT_PLACEMENT_HALF_SCREEN_IMAGE_LOAD_FAIL` | 半屏图片加载失败。 |
566
+ | `PRODUCT_PLACEMENT_MEDIA_LOAD_FAIL` | 通用素材加载失败兜底错误。 |
567
+
568
+ 素材加载失败时,组件会先触发 `error`,再触发 `close`,且 `close.detail.reason` 为 `mediaLoadFail`。
569
+
570
+ ### 12.3 动作错误码
571
+
572
+ | 错误码 | 说明 |
573
+ | --- | --- |
574
+ | `PRODUCT_PLACEMENT_ACTION_INVALID` | 动作类型或目标参数不完整。 |
575
+ | `PRODUCT_PLACEMENT_ACTION_API_UNSUPPORTED` | 当前运行环境不支持动作所需的微信 API。 |
576
+ | `PRODUCT_PLACEMENT_NAVIGATE_MINI_PROGRAM_FAIL` | 跳转目标小程序失败。 |
577
+ | `PRODUCT_PLACEMENT_NAVIGATE_FAIL` | 跳转当前小程序页面失败。 |
578
+ | `PRODUCT_PLACEMENT_API_REQUEST_FAIL` | 动态动作参数无效、请求失败或响应异常。 |
579
+ | `PRODUCT_PLACEMENT_BUSINESS_UNSUPPORTED` | `businessCode` 不在组件白名单中。 |
580
+ | `PRODUCT_PLACEMENT_BUSINESS_PARAM_INVALID` | 产品业务缺少必要的运行时参数。 |
581
+ | `PRODUCT_PLACEMENT_BUSINESS_SERVICE_NOT_READY` | 对应的 services 业务能力未初始化或版本不兼容。 |
582
+ | `PRODUCT_PLACEMENT_BUSINESS_EXECUTE_FAIL` | 产品业务服务返回失败结果。 |
583
+ | `PRODUCT_PLACEMENT_BUSINESS_EXECUTE_EXCEPTION` | 产品业务服务执行期间发生异常。 |
584
+ | `PRODUCT_PLACEMENT_BUSINESS_RESULT_INVALID` | 产品业务服务返回了组件无法处理的结果类型。 |
585
+ | `PRODUCT_PLACEMENT_ACTION_EXECUTE_EXCEPTION` | 标准动作执行器发生未捕获异常。 |
586
+
587
+ 建议至少监听 `error` 和 `close`:`error` 用于日志与异常上报,`close` 用于同步页面显隐状态。用户可感知的错误提示应由接入方结合业务场景处理。
588
+
589
+ ## 13. 安全边界
590
+
591
+ - 远程配置不能下发 WXML、JavaScript、组件名称或任意 CSS 类名。
592
+ - `displayMode`、`systemType`、动作类型、皮肤和 `businessCode` 均受白名单约束。
593
+ - `action.params` 会过滤对象、数组、函数以及非有限数值,只保留安全的基础类型。
594
+ - `token`、`authorization`、`password`、`secret` 和宿主运行时身份字段不会从远程 `action.params` 透传。
595
+ - `phone`、`userId`、`equipmentValue`、`entrance`、外部平台身份和 `extendData` 等参数必须由接入方在运行时通过 `action-context` 提供。
596
+ - `business` 动作只允许调用组件内置适配器,不会根据远程字符串动态访问 SDK 方法。
597
+
598
+ ## 14. 接入验收清单
599
+
600
+ - 已安装三个依赖包并在微信开发者工具中完成 npm 构建。
601
+ - 已初始化 Nova SDK、注册 services,并在组件渲染前挂载 SDK 实例。
602
+ - 已配置 `https://nad.ehuoke.com` 为 `request 合法域名`。
603
+ - 使用了我方分配的 `ad-id`,且 `type` 与后台配置一致。
604
+ - 已按动作类型提供完整、真实的 `action-context`。
605
+ - 已监听 `error`、`close`;需要埋点时同时监听 `load`、`exposure`、`action` 和 `actionsuccess`。
606
+ - 可能打开 H5 时,已实现 `openWebview` 处理并配置 WebView 业务域名。
607
+ - 已在真机验证图片加载、遮罩关闭、重复点击保护以及目标小程序/H5 跳转。