@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,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @Description 产品投放位动作执行器
|
|
3
|
+
* @Author wutao
|
|
4
|
+
* @Date 2026-09-14
|
|
5
|
+
* @LastEditors wutao
|
|
6
|
+
* @LastEditTime 2026-09-15 12:02:24
|
|
7
|
+
*/
|
|
8
|
+
const { executeNavigationAction } = require('../shared/action-executor')
|
|
9
|
+
|
|
10
|
+
/** 产品投放位动作错误码 */
|
|
11
|
+
const ACTION_ERROR_CODES = {
|
|
12
|
+
invalidAction: 'PRODUCT_PLACEMENT_ACTION_INVALID',
|
|
13
|
+
miniProgramFail: 'PRODUCT_PLACEMENT_NAVIGATE_MINI_PROGRAM_FAIL',
|
|
14
|
+
internalFail: 'PRODUCT_PLACEMENT_NAVIGATE_FAIL',
|
|
15
|
+
apiRequestFail: 'PRODUCT_PLACEMENT_API_REQUEST_FAIL',
|
|
16
|
+
unsupportedApi: 'PRODUCT_PLACEMENT_ACTION_API_UNSUPPORTED'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 上报业务动作执行错误
|
|
21
|
+
* @param {Record<string, any>} options 执行参数
|
|
22
|
+
* @param {string} errCode 错误码
|
|
23
|
+
* @param {string} errMsg 错误信息
|
|
24
|
+
* @returns {false}
|
|
25
|
+
*/
|
|
26
|
+
function emitBusinessError(options, errCode, errMsg) {
|
|
27
|
+
if (typeof options.onError === 'function') {
|
|
28
|
+
options.onError({ errCode, errMsg })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 获取当前标准化业务编码
|
|
36
|
+
* @param {Record<string, any>} options 执行参数
|
|
37
|
+
* @returns {string} 小写业务编码
|
|
38
|
+
*/
|
|
39
|
+
function getBusinessCode(options) {
|
|
40
|
+
return String(
|
|
41
|
+
options.action && options.action.businessCode || ''
|
|
42
|
+
).trim().toLowerCase()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 处理京东保险服务结果
|
|
47
|
+
* @param {Record<string, any>} options 执行参数
|
|
48
|
+
* @param {Record<string, any>} result 服务结果
|
|
49
|
+
* @returns {void}
|
|
50
|
+
*/
|
|
51
|
+
function handleJdInsuranceResult(options, result) {
|
|
52
|
+
const businessCode = getBusinessCode(options)
|
|
53
|
+
|
|
54
|
+
if (!result || result.code !== 0) {
|
|
55
|
+
emitBusinessError(
|
|
56
|
+
options,
|
|
57
|
+
'PRODUCT_PLACEMENT_BUSINESS_EXECUTE_FAIL',
|
|
58
|
+
result && result.message || '京东保险服务执行失败'
|
|
59
|
+
)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const data = result.data || {}
|
|
64
|
+
|
|
65
|
+
if (data.type === 'copy_success') {
|
|
66
|
+
const wxApi = typeof wx === 'undefined' ? null : wx
|
|
67
|
+
|
|
68
|
+
if (wxApi && typeof wxApi.showToast === 'function') {
|
|
69
|
+
wxApi.showToast({
|
|
70
|
+
title: result.message || '口令已复制',
|
|
71
|
+
icon: 'success'
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof options.onSuccess === 'function') {
|
|
76
|
+
options.onSuccess({
|
|
77
|
+
actionType: 'business',
|
|
78
|
+
businessCode,
|
|
79
|
+
resultType: 'copy_success'
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (data.type === 'mini_program') {
|
|
86
|
+
executeNavigationAction({
|
|
87
|
+
...options,
|
|
88
|
+
action: {
|
|
89
|
+
type: 'mini_program',
|
|
90
|
+
appId: data.appId,
|
|
91
|
+
path: data.path
|
|
92
|
+
},
|
|
93
|
+
errorCodes: ACTION_ERROR_CODES,
|
|
94
|
+
onSuccess(detail) {
|
|
95
|
+
if (typeof options.onSuccess === 'function') {
|
|
96
|
+
options.onSuccess({
|
|
97
|
+
...detail,
|
|
98
|
+
actionType: 'business',
|
|
99
|
+
businessCode
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (data.type === 'h5') {
|
|
108
|
+
executeNavigationAction({
|
|
109
|
+
...options,
|
|
110
|
+
action: {
|
|
111
|
+
type: 'web',
|
|
112
|
+
url: data.url
|
|
113
|
+
},
|
|
114
|
+
errorCodes: ACTION_ERROR_CODES,
|
|
115
|
+
onSuccess(detail) {
|
|
116
|
+
if (typeof options.onSuccess === 'function') {
|
|
117
|
+
options.onSuccess({
|
|
118
|
+
...detail,
|
|
119
|
+
actionType: 'business',
|
|
120
|
+
businessCode
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
emitBusinessError(
|
|
129
|
+
options,
|
|
130
|
+
'PRODUCT_PLACEMENT_BUSINESS_RESULT_INVALID',
|
|
131
|
+
`京东保险服务返回了不支持的结果类型:${data.type || 'empty'}`
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 执行京东保险内部服务
|
|
137
|
+
* @param {Record<string, any>} options 执行参数
|
|
138
|
+
* @returns {boolean}
|
|
139
|
+
*/
|
|
140
|
+
function executeJdInsurance(options) {
|
|
141
|
+
const sdk = options.sdk
|
|
142
|
+
const actionContext = options.actionContext || {}
|
|
143
|
+
const equipmentValue = String(actionContext.equipmentValue || '').trim()
|
|
144
|
+
const phone = String(actionContext.phone || '').trim()
|
|
145
|
+
const entrance = String(actionContext.entrance || '').trim()
|
|
146
|
+
|
|
147
|
+
if (!equipmentValue) {
|
|
148
|
+
return emitBusinessError(
|
|
149
|
+
options,
|
|
150
|
+
'PRODUCT_PLACEMENT_BUSINESS_PARAM_INVALID',
|
|
151
|
+
'京东保险服务缺少必要参数:equipmentValue'
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!sdk || !sdk.jd || typeof sdk.jd.handleJdInsurance !== 'function') {
|
|
156
|
+
return emitBusinessError(
|
|
157
|
+
options,
|
|
158
|
+
'PRODUCT_PLACEMENT_BUSINESS_SERVICE_NOT_READY',
|
|
159
|
+
'未获取到京东保险服务,请升级并初始化 @bi-nova/services'
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
Promise.resolve(sdk.jd.handleJdInsurance({
|
|
164
|
+
equipmentValue,
|
|
165
|
+
...(phone ? { phone } : {}),
|
|
166
|
+
...(entrance ? { entrance } : {})
|
|
167
|
+
})).then((result) => {
|
|
168
|
+
handleJdInsuranceResult(options, result)
|
|
169
|
+
}).catch((error) => {
|
|
170
|
+
emitBusinessError(
|
|
171
|
+
options,
|
|
172
|
+
'PRODUCT_PLACEMENT_BUSINESS_EXECUTE_EXCEPTION',
|
|
173
|
+
error && error.message || '京东保险服务执行异常'
|
|
174
|
+
)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
return true
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 执行京东保险外部对接服务
|
|
182
|
+
* @param {Record<string, any>} options 执行参数
|
|
183
|
+
* @returns {boolean}
|
|
184
|
+
*/
|
|
185
|
+
function executeJdInsuranceExt(options) {
|
|
186
|
+
const sdk = options.sdk
|
|
187
|
+
const actionContext = options.actionContext || {}
|
|
188
|
+
const thirdPlatform = String(actionContext.thirdPlatform || '').trim()
|
|
189
|
+
const thirdPlatformUserId = String(actionContext.thirdPlatformUserId || '').trim()
|
|
190
|
+
const thirdPlatformEquipmentValue = String(
|
|
191
|
+
actionContext.thirdPlatformEquipmentValue || ''
|
|
192
|
+
).trim()
|
|
193
|
+
const phone = String(actionContext.phone || '').trim()
|
|
194
|
+
const extendData = actionContext.extendData
|
|
195
|
+
const missingParamNames = [
|
|
196
|
+
['thirdPlatform', thirdPlatform],
|
|
197
|
+
['thirdPlatformUserId', thirdPlatformUserId],
|
|
198
|
+
['thirdPlatformEquipmentValue', thirdPlatformEquipmentValue]
|
|
199
|
+
].filter((item) => !item[1]).map((item) => item[0])
|
|
200
|
+
|
|
201
|
+
if (missingParamNames.length) {
|
|
202
|
+
return emitBusinessError(
|
|
203
|
+
options,
|
|
204
|
+
'PRODUCT_PLACEMENT_BUSINESS_PARAM_INVALID',
|
|
205
|
+
`京东保险外部对接服务缺少必要参数:${missingParamNames.join('、')}`
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!sdk || !sdk.jd || typeof sdk.jd.handleJdInsuranceExt !== 'function') {
|
|
210
|
+
return emitBusinessError(
|
|
211
|
+
options,
|
|
212
|
+
'PRODUCT_PLACEMENT_BUSINESS_SERVICE_NOT_READY',
|
|
213
|
+
'未获取到京东保险外部对接服务,请升级并初始化 @bi-nova/services'
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
Promise.resolve(sdk.jd.handleJdInsuranceExt({
|
|
218
|
+
thirdPlatform,
|
|
219
|
+
thirdPlatformUserId,
|
|
220
|
+
thirdPlatformEquipmentValue,
|
|
221
|
+
...(phone ? { phone } : {}),
|
|
222
|
+
...(extendData && typeof extendData === 'object' && !Array.isArray(extendData)
|
|
223
|
+
? { extendData: { ...extendData } }
|
|
224
|
+
: {})
|
|
225
|
+
})).then((result) => {
|
|
226
|
+
handleJdInsuranceResult(options, result)
|
|
227
|
+
}).catch((error) => {
|
|
228
|
+
emitBusinessError(
|
|
229
|
+
options,
|
|
230
|
+
'PRODUCT_PLACEMENT_BUSINESS_EXECUTE_EXCEPTION',
|
|
231
|
+
error && error.message || '京东保险外部对接服务执行异常'
|
|
232
|
+
)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
return true
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** 产品业务动作白名单 */
|
|
239
|
+
const BUSINESS_ACTION_HANDLERS = {
|
|
240
|
+
jd_insurance: executeJdInsurance,
|
|
241
|
+
jd_insurance_ext: executeJdInsuranceExt
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 执行产品业务动作
|
|
246
|
+
* @param {Record<string, any>} options 执行参数
|
|
247
|
+
* @returns {boolean}
|
|
248
|
+
*/
|
|
249
|
+
function executeBusinessAction(options) {
|
|
250
|
+
const businessCode = getBusinessCode(options)
|
|
251
|
+
const handler = BUSINESS_ACTION_HANDLERS[businessCode]
|
|
252
|
+
|
|
253
|
+
if (!businessCode || typeof handler !== 'function') {
|
|
254
|
+
return emitBusinessError(
|
|
255
|
+
options,
|
|
256
|
+
'PRODUCT_PLACEMENT_BUSINESS_UNSUPPORTED',
|
|
257
|
+
`不支持的产品业务:${businessCode || 'empty'}`
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return handler(options)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 执行产品投放位标准动作
|
|
266
|
+
* @param {Object} options 动作执行参数
|
|
267
|
+
* @param {Record<string, any>} options.action 标准动作配置
|
|
268
|
+
* @param {Record<string, any>} [options.sdk] Nova SDK 实例
|
|
269
|
+
* @param {Record<string, any>} [options.actionContext] 宿主动作运行时上下文
|
|
270
|
+
* @param {Record<string, any>} [options.apiRequestParams] 动态接口运行时参数
|
|
271
|
+
* @param {(detail: Record<string, any>) => void} [options.onOpenWebview] WebView 回调
|
|
272
|
+
* @param {(detail: Record<string, any>) => void} [options.onSuccess] 成功回调
|
|
273
|
+
* @param {(detail: {errCode: string, errMsg: string}) => void} [options.onError] 失败回调
|
|
274
|
+
* @returns {boolean} 是否已进入动作执行流程
|
|
275
|
+
*/
|
|
276
|
+
function executeProductPlacementAction(options) {
|
|
277
|
+
const action = options && options.action || {}
|
|
278
|
+
|
|
279
|
+
if (action.type === 'business') {
|
|
280
|
+
return executeBusinessAction(options)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return executeNavigationAction({
|
|
284
|
+
...options,
|
|
285
|
+
errorCodes: ACTION_ERROR_CODES
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = {
|
|
290
|
+
ACTION_ERROR_CODES,
|
|
291
|
+
BUSINESS_ACTION_HANDLERS,
|
|
292
|
+
executeProductPlacementAction
|
|
293
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @Description 产品投放位 Banner 展示组件
|
|
3
|
+
* @Author wutao
|
|
4
|
+
* @Date 2026-09-08
|
|
5
|
+
* @LastEditors wutao
|
|
6
|
+
* @LastEditTime 2026-09-08 17:55:14
|
|
7
|
+
*/
|
|
8
|
+
Component({
|
|
9
|
+
properties: {
|
|
10
|
+
creative: { type: Object, value: null },
|
|
11
|
+
panelStyle: { type: Object, value: null },
|
|
12
|
+
actionable: { type: Boolean, value: false },
|
|
13
|
+
actionLoading: { type: Boolean, value: false }
|
|
14
|
+
},
|
|
15
|
+
data: {
|
|
16
|
+
ready: false
|
|
17
|
+
},
|
|
18
|
+
lifetimes: {
|
|
19
|
+
attached() {
|
|
20
|
+
this.syncCreative(this.properties.creative)
|
|
21
|
+
},
|
|
22
|
+
detached() {
|
|
23
|
+
this.clearReadyTimer()
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
observers: {
|
|
27
|
+
creative(nextCreative) {
|
|
28
|
+
this.syncCreative(nextCreative)
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
methods: {
|
|
32
|
+
/**
|
|
33
|
+
* 素材不包含图片时,在视图完成更新后直接进入可展示状态
|
|
34
|
+
* @param {Record<string, string> | null} creative 标准化素材
|
|
35
|
+
* @returns {void}
|
|
36
|
+
*/
|
|
37
|
+
syncCreative(creative) {
|
|
38
|
+
this.clearReadyTimer()
|
|
39
|
+
this.setData({ ready: false })
|
|
40
|
+
|
|
41
|
+
if (creative && creative.imageUrl) return
|
|
42
|
+
|
|
43
|
+
this.readyTimer = setTimeout(() => this.notifyReady('content'), 0)
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/** 清理延迟就绪任务 */
|
|
47
|
+
clearReadyTimer() {
|
|
48
|
+
if (!this.readyTimer) return
|
|
49
|
+
|
|
50
|
+
clearTimeout(this.readyTimer)
|
|
51
|
+
this.readyTimer = null
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 通知上层组件素材已经可以展示
|
|
56
|
+
* @param {string} mediaType 素材类型
|
|
57
|
+
* @returns {void}
|
|
58
|
+
*/
|
|
59
|
+
notifyReady(mediaType) {
|
|
60
|
+
if (this.data.ready) return
|
|
61
|
+
|
|
62
|
+
this.setData({ ready: true })
|
|
63
|
+
this.triggerEvent('ready', { mediaType })
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/** 处理图片加载完成 */
|
|
67
|
+
handleImageLoad() {
|
|
68
|
+
this.notifyReady('image')
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 处理图片加载失败
|
|
73
|
+
* @param {Record<string, any>} event 小程序图片错误事件
|
|
74
|
+
* @returns {void}
|
|
75
|
+
*/
|
|
76
|
+
handleImageError(event) {
|
|
77
|
+
const rawDetail = event && event.detail || {}
|
|
78
|
+
|
|
79
|
+
this.triggerEvent('error', {
|
|
80
|
+
errCode: 'PRODUCT_PLACEMENT_BANNER_IMAGE_LOAD_FAIL',
|
|
81
|
+
errMsg: '产品 Banner 图片加载失败',
|
|
82
|
+
errorType: 'MEDIA_LOAD_FAIL',
|
|
83
|
+
rawErrCode: rawDetail.errCode,
|
|
84
|
+
rawErrMsg: rawDetail.errMsg
|
|
85
|
+
})
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/** 处理关闭按钮点击 */
|
|
89
|
+
handleClose() {
|
|
90
|
+
this.triggerEvent('close', { reason: 'closeButton' })
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 处理可点击区域操作
|
|
95
|
+
* @param {Record<string, any>} event 点击事件
|
|
96
|
+
* @returns {void}
|
|
97
|
+
*/
|
|
98
|
+
handleAction(event) {
|
|
99
|
+
if (!this.properties.actionable || this.properties.actionLoading) return
|
|
100
|
+
|
|
101
|
+
this.triggerEvent('action', {
|
|
102
|
+
source: event && event.currentTarget && event.currentTarget.dataset.source || 'surface'
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@Description 产品投放位 Banner 展示组件模板
|
|
3
|
+
@Author wutao
|
|
4
|
+
@Date 2026-09-08
|
|
5
|
+
@LastEditors wutao
|
|
6
|
+
@LastEditTime 2026-09-14 14:44:07
|
|
7
|
+
-->
|
|
8
|
+
<view class="bi-product-banner {{panelStyle.skinClass}}">
|
|
9
|
+
<view
|
|
10
|
+
class="bi-product-banner__surface {{actionable ? 'bi-product-banner__surface--actionable' : ''}}"
|
|
11
|
+
data-source="surface"
|
|
12
|
+
bindtap="handleAction"
|
|
13
|
+
>
|
|
14
|
+
<image
|
|
15
|
+
wx:if="{{creative.imageUrl}}"
|
|
16
|
+
class="bi-product-banner__image"
|
|
17
|
+
src="{{creative.imageUrl}}"
|
|
18
|
+
mode="widthFix"
|
|
19
|
+
bindload="handleImageLoad"
|
|
20
|
+
binderror="handleImageError"
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
<view wx:if="{{creative.title || creative.description || creative.buttonText}}" class="bi-product-banner__body">
|
|
24
|
+
<view class="bi-product-banner__copy">
|
|
25
|
+
<text wx:if="{{creative.badgeText}}" class="bi-product-banner__badge">{{creative.badgeText}}</text>
|
|
26
|
+
<text wx:if="{{creative.title}}" class="bi-product-banner__title">{{creative.title}}</text>
|
|
27
|
+
<text wx:if="{{creative.description}}" class="bi-product-banner__description">{{creative.description}}</text>
|
|
28
|
+
</view>
|
|
29
|
+
<button
|
|
30
|
+
wx:if="{{creative.buttonText}}"
|
|
31
|
+
class="bi-product-banner__button {{!actionable || actionLoading ? 'bi-product-banner__button--disabled' : ''}}"
|
|
32
|
+
data-source="button"
|
|
33
|
+
loading="{{actionLoading}}"
|
|
34
|
+
disabled="{{!actionable || actionLoading}}"
|
|
35
|
+
catchtap="handleAction"
|
|
36
|
+
>
|
|
37
|
+
{{creative.buttonText}}
|
|
38
|
+
</button>
|
|
39
|
+
</view>
|
|
40
|
+
</view>
|
|
41
|
+
|
|
42
|
+
<button
|
|
43
|
+
wx:if="{{panelStyle.closeable}}"
|
|
44
|
+
class="bi-product-banner__close"
|
|
45
|
+
aria-label="关闭产品投放位"
|
|
46
|
+
catchtap="handleClose"
|
|
47
|
+
>×</button>
|
|
48
|
+
</view>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Description 产品投放位 Banner 展示组件样式
|
|
3
|
+
* @Author wutao
|
|
4
|
+
* @Date 2026-09-08
|
|
5
|
+
* @LastEditors wutao
|
|
6
|
+
* @LastEditTime 2026-09-14 14:44:07
|
|
7
|
+
*/
|
|
8
|
+
:host {
|
|
9
|
+
display: block;
|
|
10
|
+
width: 100%;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.bi-product-banner {
|
|
14
|
+
position: relative;
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
width: 100%;
|
|
18
|
+
border-radius: 16rpx;
|
|
19
|
+
background: #ffffff;
|
|
20
|
+
box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.08);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.bi-product-banner__surface--actionable {
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.bi-product-banner__surface {
|
|
28
|
+
width: 100%;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.bi-product-banner__image {
|
|
32
|
+
display: block;
|
|
33
|
+
width: 100%;
|
|
34
|
+
height: auto;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.bi-product-banner__body {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
box-sizing: border-box;
|
|
41
|
+
min-height: 132rpx;
|
|
42
|
+
padding: 24rpx 28rpx;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.bi-product-banner__copy {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex: 1;
|
|
48
|
+
min-width: 0;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.bi-product-banner__badge {
|
|
53
|
+
align-self: flex-start;
|
|
54
|
+
padding: 2rpx 10rpx;
|
|
55
|
+
border-radius: 6rpx;
|
|
56
|
+
background: #fff0e8;
|
|
57
|
+
color: #fa541c;
|
|
58
|
+
font-size: 20rpx;
|
|
59
|
+
line-height: 32rpx;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.bi-product-banner__title {
|
|
63
|
+
margin-top: 8rpx;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
color: #1f2329;
|
|
66
|
+
font-size: 30rpx;
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
line-height: 42rpx;
|
|
69
|
+
text-overflow: ellipsis;
|
|
70
|
+
white-space: nowrap;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.bi-product-banner__description {
|
|
74
|
+
display: -webkit-box;
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
color: #646a73;
|
|
77
|
+
font-size: 24rpx;
|
|
78
|
+
line-height: 34rpx;
|
|
79
|
+
-webkit-box-orient: vertical;
|
|
80
|
+
-webkit-line-clamp: 2;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.bi-product-banner__button {
|
|
84
|
+
flex: 0 0 auto;
|
|
85
|
+
box-sizing: border-box;
|
|
86
|
+
min-width: 144rpx;
|
|
87
|
+
height: 64rpx;
|
|
88
|
+
margin: 0;
|
|
89
|
+
margin-left: 24rpx;
|
|
90
|
+
padding: 0 24rpx;
|
|
91
|
+
border-radius: 32rpx;
|
|
92
|
+
background: #1677ff;
|
|
93
|
+
color: #ffffff;
|
|
94
|
+
font-size: 24rpx;
|
|
95
|
+
font-weight: 500;
|
|
96
|
+
line-height: 64rpx;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.bi-product-banner__button::after,
|
|
100
|
+
.bi-product-banner__close::after {
|
|
101
|
+
border: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.bi-product-banner__close {
|
|
105
|
+
position: absolute;
|
|
106
|
+
top: 12rpx;
|
|
107
|
+
right: 12rpx;
|
|
108
|
+
z-index: 2;
|
|
109
|
+
width: 48rpx;
|
|
110
|
+
height: 48rpx;
|
|
111
|
+
margin: 0;
|
|
112
|
+
padding: 0;
|
|
113
|
+
border-radius: 50%;
|
|
114
|
+
background: rgba(0, 0, 0, 0.42);
|
|
115
|
+
color: #ffffff;
|
|
116
|
+
font-size: 34rpx;
|
|
117
|
+
font-weight: 300;
|
|
118
|
+
line-height: 44rpx;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.bi-product-skin--vip-gold .bi-product-banner__button {
|
|
122
|
+
background: linear-gradient(135deg, #efc77e 0%, #b8893f 100%);
|
|
123
|
+
color: #35230b;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.bi-product-skin--insurance-blue .bi-product-banner__button {
|
|
127
|
+
background: #246bfd;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.bi-product-banner__button.bi-product-banner__button--disabled {
|
|
131
|
+
background: #a9c9f7;
|
|
132
|
+
color: rgba(255, 255, 255, 0.9);
|
|
133
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
properties: {
|
|
10
|
+
creative: { type: Object, value: null },
|
|
11
|
+
panelStyle: { type: Object, value: null },
|
|
12
|
+
actionable: { type: Boolean, value: false },
|
|
13
|
+
actionLoading: { type: Boolean, value: false },
|
|
14
|
+
zIndex: { type: Number, value: 999 }
|
|
15
|
+
},
|
|
16
|
+
data: {
|
|
17
|
+
ready: false,
|
|
18
|
+
panelHeight: 68
|
|
19
|
+
},
|
|
20
|
+
lifetimes: {
|
|
21
|
+
attached() {
|
|
22
|
+
this.syncPanelStyle(this.properties.panelStyle)
|
|
23
|
+
this.syncCreative(this.properties.creative)
|
|
24
|
+
},
|
|
25
|
+
detached() {
|
|
26
|
+
this.clearReadyTimer()
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
observers: {
|
|
30
|
+
creative(nextCreative) {
|
|
31
|
+
this.syncCreative(nextCreative)
|
|
32
|
+
},
|
|
33
|
+
panelStyle(nextPanelStyle) {
|
|
34
|
+
this.syncPanelStyle(nextPanelStyle)
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
methods: {
|
|
38
|
+
/**
|
|
39
|
+
* 同步半屏高度,输入值已经由统一入口限制在安全范围
|
|
40
|
+
* @param {Record<string, any> | null} panelStyle 标准化面板样式
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
syncPanelStyle(panelStyle) {
|
|
44
|
+
const heightRatio = Number(panelStyle && panelStyle.heightRatio)
|
|
45
|
+
this.setData({
|
|
46
|
+
panelHeight: Number.isFinite(heightRatio) ? Math.round(heightRatio * 100) : 68
|
|
47
|
+
})
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 同步半屏素材状态
|
|
52
|
+
* @param {Record<string, string> | null} creative 标准化素材
|
|
53
|
+
* @returns {void}
|
|
54
|
+
*/
|
|
55
|
+
syncCreative(creative) {
|
|
56
|
+
this.clearReadyTimer()
|
|
57
|
+
this.setData({ ready: false })
|
|
58
|
+
|
|
59
|
+
if (creative && creative.imageUrl) return
|
|
60
|
+
|
|
61
|
+
this.readyTimer = setTimeout(() => this.notifyReady('content'), 0)
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/** 清理延迟就绪任务 */
|
|
65
|
+
clearReadyTimer() {
|
|
66
|
+
if (!this.readyTimer) return
|
|
67
|
+
|
|
68
|
+
clearTimeout(this.readyTimer)
|
|
69
|
+
this.readyTimer = null
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 通知上层组件半屏已经可以展示
|
|
74
|
+
* @param {string} mediaType 素材类型
|
|
75
|
+
* @returns {void}
|
|
76
|
+
*/
|
|
77
|
+
notifyReady(mediaType) {
|
|
78
|
+
if (this.data.ready) return
|
|
79
|
+
|
|
80
|
+
this.setData({ ready: true })
|
|
81
|
+
this.triggerEvent('ready', { mediaType })
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/** 处理图片加载完成 */
|
|
85
|
+
handleImageLoad() {
|
|
86
|
+
this.notifyReady('image')
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 处理图片加载失败
|
|
91
|
+
* @param {Record<string, any>} event 小程序图片错误事件
|
|
92
|
+
* @returns {void}
|
|
93
|
+
*/
|
|
94
|
+
handleImageError(event) {
|
|
95
|
+
const rawDetail = event && event.detail || {}
|
|
96
|
+
|
|
97
|
+
this.triggerEvent('error', {
|
|
98
|
+
errCode: 'PRODUCT_PLACEMENT_HALF_SCREEN_IMAGE_LOAD_FAIL',
|
|
99
|
+
errMsg: '产品半屏图片加载失败',
|
|
100
|
+
errorType: 'MEDIA_LOAD_FAIL',
|
|
101
|
+
rawErrCode: rawDetail.errCode,
|
|
102
|
+
rawErrMsg: rawDetail.errMsg
|
|
103
|
+
})
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/** 处理关闭按钮点击 */
|
|
107
|
+
handleClose() {
|
|
108
|
+
this.triggerEvent('close', { reason: 'closeButton' })
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
/** 处理遮罩关闭事件 */
|
|
112
|
+
handleMaskClose() {
|
|
113
|
+
this.triggerEvent('close', { reason: 'mask' })
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 处理产品动作
|
|
118
|
+
* @param {Record<string, any>} event 点击事件
|
|
119
|
+
* @returns {void}
|
|
120
|
+
*/
|
|
121
|
+
handleAction(event) {
|
|
122
|
+
if (!this.properties.actionable || this.properties.actionLoading) return
|
|
123
|
+
|
|
124
|
+
this.triggerEvent('action', {
|
|
125
|
+
source: event && event.currentTarget && event.currentTarget.dataset.source || 'button'
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
})
|