@bi-nova/ui-weapp 1.2.1 → 1.3.0-beta.202609151700
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/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,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Description 产品投放位通用遮罩组件样式
|
|
3
|
+
* @Author wutao
|
|
4
|
+
* @Date 2026-09-08
|
|
5
|
+
* @LastEditors wutao
|
|
6
|
+
* @LastEditTime 2026-09-08 17:55:14
|
|
7
|
+
*/
|
|
8
|
+
.bi-product-overlay {
|
|
9
|
+
position: fixed;
|
|
10
|
+
top: 0;
|
|
11
|
+
right: 0;
|
|
12
|
+
bottom: 0;
|
|
13
|
+
left: 0;
|
|
14
|
+
display: flex;
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
width: 100vw;
|
|
17
|
+
height: 100vh;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.bi-product-overlay--center {
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
padding: 48rpx;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.bi-product-overlay--bottom {
|
|
27
|
+
align-items: flex-end;
|
|
28
|
+
justify-content: stretch;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.bi-product-overlay__mask {
|
|
32
|
+
position: absolute;
|
|
33
|
+
top: 0;
|
|
34
|
+
right: 0;
|
|
35
|
+
bottom: 0;
|
|
36
|
+
left: 0;
|
|
37
|
+
background: rgba(0, 0, 0, 0.56);
|
|
38
|
+
animation: bi-product-overlay-fade-in 180ms ease-out both;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.bi-product-overlay__content {
|
|
42
|
+
position: relative;
|
|
43
|
+
z-index: 1;
|
|
44
|
+
width: 100%;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@keyframes bi-product-overlay-fade-in {
|
|
48
|
+
from {
|
|
49
|
+
opacity: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
to {
|
|
53
|
+
opacity: 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
},
|
|
19
|
+
lifetimes: {
|
|
20
|
+
attached() {
|
|
21
|
+
this.syncCreative(this.properties.creative)
|
|
22
|
+
},
|
|
23
|
+
detached() {
|
|
24
|
+
this.clearReadyTimer()
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
observers: {
|
|
28
|
+
creative(nextCreative) {
|
|
29
|
+
this.syncCreative(nextCreative)
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
methods: {
|
|
33
|
+
/**
|
|
34
|
+
* 同步弹窗素材状态
|
|
35
|
+
* @param {Record<string, string> | null} creative 标准化素材
|
|
36
|
+
* @returns {void}
|
|
37
|
+
*/
|
|
38
|
+
syncCreative(creative) {
|
|
39
|
+
this.clearReadyTimer()
|
|
40
|
+
this.setData({ ready: false })
|
|
41
|
+
|
|
42
|
+
if (creative && creative.imageUrl) return
|
|
43
|
+
|
|
44
|
+
this.readyTimer = setTimeout(() => this.notifyReady('content'), 0)
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/** 清理延迟就绪任务 */
|
|
48
|
+
clearReadyTimer() {
|
|
49
|
+
if (!this.readyTimer) return
|
|
50
|
+
|
|
51
|
+
clearTimeout(this.readyTimer)
|
|
52
|
+
this.readyTimer = null
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 通知上层组件弹窗已经可以展示
|
|
57
|
+
* @param {string} mediaType 素材类型
|
|
58
|
+
* @returns {void}
|
|
59
|
+
*/
|
|
60
|
+
notifyReady(mediaType) {
|
|
61
|
+
if (this.data.ready) return
|
|
62
|
+
|
|
63
|
+
this.setData({ ready: true })
|
|
64
|
+
this.triggerEvent('ready', { mediaType })
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/** 处理图片加载完成 */
|
|
68
|
+
handleImageLoad() {
|
|
69
|
+
this.notifyReady('image')
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 处理图片加载失败
|
|
74
|
+
* @param {Record<string, any>} event 小程序图片错误事件
|
|
75
|
+
* @returns {void}
|
|
76
|
+
*/
|
|
77
|
+
handleImageError(event) {
|
|
78
|
+
const rawDetail = event && event.detail || {}
|
|
79
|
+
|
|
80
|
+
this.triggerEvent('error', {
|
|
81
|
+
errCode: 'PRODUCT_PLACEMENT_POPUP_IMAGE_LOAD_FAIL',
|
|
82
|
+
errMsg: '产品弹窗图片加载失败',
|
|
83
|
+
errorType: 'MEDIA_LOAD_FAIL',
|
|
84
|
+
rawErrCode: rawDetail.errCode,
|
|
85
|
+
rawErrMsg: rawDetail.errMsg
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/** 处理关闭按钮点击 */
|
|
90
|
+
handleClose() {
|
|
91
|
+
this.triggerEvent('close', { reason: 'closeButton' })
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/** 处理遮罩关闭事件 */
|
|
95
|
+
handleMaskClose() {
|
|
96
|
+
this.triggerEvent('close', { reason: 'mask' })
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 处理产品动作
|
|
101
|
+
* @param {Record<string, any>} event 点击事件
|
|
102
|
+
* @returns {void}
|
|
103
|
+
*/
|
|
104
|
+
handleAction(event) {
|
|
105
|
+
if (!this.properties.actionable || this.properties.actionLoading) return
|
|
106
|
+
|
|
107
|
+
this.triggerEvent('action', {
|
|
108
|
+
source: event && event.currentTarget && event.currentTarget.dataset.source || 'surface'
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@Description 产品投放位居中弹窗组件模板
|
|
3
|
+
@Author wutao
|
|
4
|
+
@Date 2026-09-08
|
|
5
|
+
@LastEditors wutao
|
|
6
|
+
@LastEditTime 2026-09-14 14:44:07
|
|
7
|
+
-->
|
|
8
|
+
<bi-product-overlay
|
|
9
|
+
show="{{true}}"
|
|
10
|
+
position="center"
|
|
11
|
+
mask-closable="{{panelStyle.maskClosable}}"
|
|
12
|
+
z-index="{{zIndex}}"
|
|
13
|
+
bind:maskclose="handleMaskClose"
|
|
14
|
+
>
|
|
15
|
+
<view class="bi-product-popup {{panelStyle.skinClass}}">
|
|
16
|
+
<button
|
|
17
|
+
wx:if="{{panelStyle.closeable}}"
|
|
18
|
+
class="bi-product-popup__close"
|
|
19
|
+
aria-label="关闭产品弹窗"
|
|
20
|
+
catchtap="handleClose"
|
|
21
|
+
>×</button>
|
|
22
|
+
|
|
23
|
+
<view
|
|
24
|
+
class="bi-product-popup__surface {{actionable ? 'bi-product-popup__surface--actionable' : ''}}"
|
|
25
|
+
data-source="surface"
|
|
26
|
+
bindtap="handleAction"
|
|
27
|
+
>
|
|
28
|
+
<image
|
|
29
|
+
wx:if="{{creative.imageUrl}}"
|
|
30
|
+
class="bi-product-popup__image"
|
|
31
|
+
src="{{creative.imageUrl}}"
|
|
32
|
+
mode="widthFix"
|
|
33
|
+
bindload="handleImageLoad"
|
|
34
|
+
binderror="handleImageError"
|
|
35
|
+
/>
|
|
36
|
+
|
|
37
|
+
<view wx:if="{{creative.title || creative.description || creative.buttonText}}" class="bi-product-popup__body">
|
|
38
|
+
<text wx:if="{{creative.badgeText}}" class="bi-product-popup__badge">{{creative.badgeText}}</text>
|
|
39
|
+
<text wx:if="{{creative.title}}" class="bi-product-popup__title">{{creative.title}}</text>
|
|
40
|
+
<text wx:if="{{creative.description}}" class="bi-product-popup__description">{{creative.description}}</text>
|
|
41
|
+
<button
|
|
42
|
+
wx:if="{{creative.buttonText}}"
|
|
43
|
+
class="bi-product-popup__button {{!actionable || actionLoading ? 'bi-product-popup__button--disabled' : ''}}"
|
|
44
|
+
data-source="button"
|
|
45
|
+
loading="{{actionLoading}}"
|
|
46
|
+
disabled="{{!actionable || actionLoading}}"
|
|
47
|
+
catchtap="handleAction"
|
|
48
|
+
>
|
|
49
|
+
{{creative.buttonText}}
|
|
50
|
+
</button>
|
|
51
|
+
</view>
|
|
52
|
+
</view>
|
|
53
|
+
</view>
|
|
54
|
+
</bi-product-overlay>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Description 产品投放位居中弹窗组件样式
|
|
3
|
+
* @Author wutao
|
|
4
|
+
* @Date 2026-09-08
|
|
5
|
+
* @LastEditors wutao
|
|
6
|
+
* @LastEditTime 2026-09-14 14:44:07
|
|
7
|
+
*/
|
|
8
|
+
.bi-product-popup {
|
|
9
|
+
position: relative;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
width: 100%;
|
|
13
|
+
max-width: 620rpx;
|
|
14
|
+
margin: 0 auto;
|
|
15
|
+
border-radius: 28rpx;
|
|
16
|
+
background: #ffffff;
|
|
17
|
+
box-shadow: 0 24rpx 64rpx rgba(0, 0, 0, 0.24);
|
|
18
|
+
animation: bi-product-popup-enter 220ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.bi-product-popup__surface--actionable {
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.bi-product-popup__image {
|
|
26
|
+
display: block;
|
|
27
|
+
width: 100%;
|
|
28
|
+
height: auto;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.bi-product-popup__body {
|
|
32
|
+
display: flex;
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
padding: 36rpx 40rpx 40rpx;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
align-items: center;
|
|
37
|
+
text-align: center;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.bi-product-popup__badge {
|
|
41
|
+
margin-bottom: 16rpx;
|
|
42
|
+
padding: 4rpx 14rpx;
|
|
43
|
+
border-radius: 8rpx;
|
|
44
|
+
background: #fff0e8;
|
|
45
|
+
color: #fa541c;
|
|
46
|
+
font-size: 22rpx;
|
|
47
|
+
line-height: 34rpx;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.bi-product-popup__title {
|
|
51
|
+
color: #1f2329;
|
|
52
|
+
font-size: 36rpx;
|
|
53
|
+
font-weight: 600;
|
|
54
|
+
line-height: 50rpx;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.bi-product-popup__description {
|
|
58
|
+
margin-top: 16rpx;
|
|
59
|
+
color: #646a73;
|
|
60
|
+
font-size: 26rpx;
|
|
61
|
+
line-height: 40rpx;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.bi-product-popup__button {
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
width: 100%;
|
|
67
|
+
height: 88rpx;
|
|
68
|
+
margin: 32rpx 0 0;
|
|
69
|
+
padding: 0 32rpx;
|
|
70
|
+
border-radius: 44rpx;
|
|
71
|
+
background: #1677ff;
|
|
72
|
+
color: #ffffff;
|
|
73
|
+
font-size: 30rpx;
|
|
74
|
+
font-weight: 600;
|
|
75
|
+
line-height: 88rpx;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.bi-product-popup__button::after,
|
|
79
|
+
.bi-product-popup__close::after {
|
|
80
|
+
border: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.bi-product-popup__close {
|
|
84
|
+
position: absolute;
|
|
85
|
+
top: 18rpx;
|
|
86
|
+
right: 18rpx;
|
|
87
|
+
z-index: 2;
|
|
88
|
+
width: 52rpx;
|
|
89
|
+
height: 52rpx;
|
|
90
|
+
margin: 0;
|
|
91
|
+
padding: 0;
|
|
92
|
+
border-radius: 50%;
|
|
93
|
+
background: rgba(0, 0, 0, 0.42);
|
|
94
|
+
color: #ffffff;
|
|
95
|
+
font-size: 36rpx;
|
|
96
|
+
font-weight: 300;
|
|
97
|
+
line-height: 48rpx;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.bi-product-skin--vip-gold .bi-product-popup__button {
|
|
101
|
+
background: linear-gradient(135deg, #efc77e 0%, #b8893f 100%);
|
|
102
|
+
color: #35230b;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.bi-product-skin--insurance-blue .bi-product-popup__button {
|
|
106
|
+
background: #246bfd;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.bi-product-popup__button.bi-product-popup__button--disabled {
|
|
110
|
+
background: #a9c9f7;
|
|
111
|
+
color: rgba(255, 255, 255, 0.9);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@keyframes bi-product-popup-enter {
|
|
115
|
+
from {
|
|
116
|
+
opacity: 0;
|
|
117
|
+
transform: translateY(20rpx) scale(0.94);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
to {
|
|
121
|
+
opacity: 1;
|
|
122
|
+
transform: translateY(0) scale(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @Description 产品投放位配置处理工具
|
|
3
|
+
* @Author wutao
|
|
4
|
+
* @Date 2026-09-08
|
|
5
|
+
* @LastEditors wutao
|
|
6
|
+
* @LastEditTime 2026-09-15 12:02:24
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 产品投放位支持的展示形态 */
|
|
10
|
+
const SUPPORTED_DISPLAY_MODES = ['banner', 'popup', 'half_screen']
|
|
11
|
+
/** 产品投放位支持的动作类型 */
|
|
12
|
+
const SUPPORTED_ACTION_TYPES = ['business', 'mini_program', 'internal', 'web', 'api', 'none']
|
|
13
|
+
/** 禁止由 action.params 下发的敏感字段及宿主运行时字段 */
|
|
14
|
+
const BLOCKED_ACTION_PARAM_NAMES = [
|
|
15
|
+
'token',
|
|
16
|
+
'authorization',
|
|
17
|
+
'password',
|
|
18
|
+
'secret',
|
|
19
|
+
'phone',
|
|
20
|
+
'userid',
|
|
21
|
+
'equipmentvalue',
|
|
22
|
+
'entrance',
|
|
23
|
+
'thirdplatform',
|
|
24
|
+
'thirdplatformuserid',
|
|
25
|
+
'thirdplatformequipmentvalue',
|
|
26
|
+
'extenddata'
|
|
27
|
+
]
|
|
28
|
+
/** 内置皮肤白名单,禁止通过配置注入任意样式类名 */
|
|
29
|
+
const SUPPORTED_SKIN_CODES = ['default', 'vip_gold', 'insurance_blue']
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 判断传入值是否为普通对象
|
|
33
|
+
* @param {unknown} value 待判断值
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
function isPlainObject(value) {
|
|
37
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 把可选配置值转换为安全字符串
|
|
42
|
+
* @param {unknown} value 原始值
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
function toSafeString(value) {
|
|
46
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 过滤业务动作参数,仅保留可安全透传的基础类型
|
|
51
|
+
* @param {unknown} params 原始动作参数
|
|
52
|
+
* @returns {Record<string, string | number | boolean>}
|
|
53
|
+
*/
|
|
54
|
+
function normalizeActionParams(params) {
|
|
55
|
+
if (!isPlainObject(params)) return {}
|
|
56
|
+
|
|
57
|
+
return Object.keys(params).reduce((result, key) => {
|
|
58
|
+
const value = params[key]
|
|
59
|
+
const normalizedKey = key.trim().toLowerCase()
|
|
60
|
+
|
|
61
|
+
if (!normalizedKey || BLOCKED_ACTION_PARAM_NAMES.includes(normalizedKey)) {
|
|
62
|
+
return result
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
['string', 'boolean'].includes(typeof value) ||
|
|
67
|
+
typeof value === 'number' && Number.isFinite(value)
|
|
68
|
+
) {
|
|
69
|
+
result[key] = value
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return result
|
|
73
|
+
}, {})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 标准化点击动作配置
|
|
78
|
+
* @param {unknown} action 原始动作配置
|
|
79
|
+
* @returns {Record<string, any>}
|
|
80
|
+
*/
|
|
81
|
+
function normalizeAction(action) {
|
|
82
|
+
const source = isPlainObject(action) ? action : {}
|
|
83
|
+
const rawType = toSafeString(source.type)
|
|
84
|
+
const normalizedType = rawType.toLowerCase()
|
|
85
|
+
const compatibleType = normalizedType === 'miniprogram' ? 'mini_program' : normalizedType
|
|
86
|
+
const type = SUPPORTED_ACTION_TYPES.includes(compatibleType) ? compatibleType : 'none'
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
type,
|
|
90
|
+
businessCode: toSafeString(source.businessCode),
|
|
91
|
+
appId: toSafeString(source.appId),
|
|
92
|
+
path: toSafeString(source.path),
|
|
93
|
+
url: toSafeString(source.url),
|
|
94
|
+
params: normalizeActionParams(source.params)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 标准化面板样式配置
|
|
100
|
+
* @param {unknown} panelStyle 原始面板样式
|
|
101
|
+
* @returns {Record<string, any>}
|
|
102
|
+
*/
|
|
103
|
+
function normalizePanelStyle(panelStyle) {
|
|
104
|
+
const source = isPlainObject(panelStyle) ? panelStyle : {}
|
|
105
|
+
const rawSkinCode = toSafeString(source.skinCode).toLowerCase()
|
|
106
|
+
const skinCode = SUPPORTED_SKIN_CODES.includes(rawSkinCode) ? rawSkinCode : 'default'
|
|
107
|
+
const rawHeightRatio = Number(source.heightRatio)
|
|
108
|
+
const heightRatio = Number.isFinite(rawHeightRatio)
|
|
109
|
+
? Math.min(0.85, Math.max(0.4, rawHeightRatio))
|
|
110
|
+
: 0.68
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
skinCode,
|
|
114
|
+
skinClass: `bi-product-skin--${skinCode.replace(/_/g, '-')}`,
|
|
115
|
+
heightRatio,
|
|
116
|
+
closeable: source.closeable === true,
|
|
117
|
+
maskClosable: source.maskClosable !== false,
|
|
118
|
+
showAgreement: source.showAgreement === true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 根据展示形态解析素材图片
|
|
124
|
+
* @param {Record<string, any>} creative 原始素材配置
|
|
125
|
+
* @param {'banner' | 'popup' | 'half_screen'} displayMode 展示形态
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
function resolveImageUrl(creative, displayMode) {
|
|
129
|
+
if (toSafeString(creative.imageUrl)) return toSafeString(creative.imageUrl)
|
|
130
|
+
if (toSafeString(creative.image)) return toSafeString(creative.image)
|
|
131
|
+
|
|
132
|
+
if (displayMode === 'banner') {
|
|
133
|
+
return toSafeString(creative.bannerImageUrl)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return toSafeString(creative.popupImageUrl)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 标准化产品素材配置
|
|
141
|
+
* @param {unknown} creative 原始素材配置
|
|
142
|
+
* @param {'banner' | 'popup' | 'half_screen'} displayMode 展示形态
|
|
143
|
+
* @returns {Record<string, string>}
|
|
144
|
+
*/
|
|
145
|
+
function normalizeCreative(creative, displayMode) {
|
|
146
|
+
const source = isPlainObject(creative) ? creative : {}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
imageUrl: resolveImageUrl(source, displayMode),
|
|
150
|
+
title: toSafeString(source.title),
|
|
151
|
+
description: toSafeString(source.description),
|
|
152
|
+
buttonText: toSafeString(source.buttonText),
|
|
153
|
+
badgeText: toSafeString(source.badgeText)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 判断素材是否包含可展示内容
|
|
159
|
+
* @param {Record<string, string>} creative 标准化素材
|
|
160
|
+
* @returns {boolean}
|
|
161
|
+
*/
|
|
162
|
+
function hasRenderableCreative(creative) {
|
|
163
|
+
return Boolean(
|
|
164
|
+
creative.imageUrl ||
|
|
165
|
+
creative.title ||
|
|
166
|
+
creative.description ||
|
|
167
|
+
creative.buttonText
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 构建配置校验失败结果
|
|
173
|
+
* @param {string} errCode 错误码
|
|
174
|
+
* @param {string} errMsg 错误说明
|
|
175
|
+
* @param {boolean} [silent] 是否静默处理
|
|
176
|
+
* @returns {{valid: false, config: null, errCode: string, errMsg: string, silent: boolean}}
|
|
177
|
+
*/
|
|
178
|
+
function buildInvalidResult(errCode, errMsg, silent) {
|
|
179
|
+
return {
|
|
180
|
+
valid: false,
|
|
181
|
+
config: null,
|
|
182
|
+
errCode,
|
|
183
|
+
errMsg,
|
|
184
|
+
silent: Boolean(silent)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 标准化产品投放位配置并校验广告 ID 约束
|
|
190
|
+
* @param {unknown} config 原始产品投放位配置
|
|
191
|
+
* @param {{adId?: string | number, type?: string}} [expectations] 调用方预期配置
|
|
192
|
+
* @returns {Record<string, any>}
|
|
193
|
+
*/
|
|
194
|
+
function normalizeProductPlacementConfig(config, expectations) {
|
|
195
|
+
if (!isPlainObject(config) || Object.keys(config).length === 0) {
|
|
196
|
+
return buildInvalidResult(
|
|
197
|
+
'PRODUCT_PLACEMENT_EMPTY_CONFIG',
|
|
198
|
+
'产品投放位配置为空',
|
|
199
|
+
true
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const expected = isPlainObject(expectations) ? expectations : {}
|
|
204
|
+
const displayMode = toSafeString(config.displayMode).toLowerCase()
|
|
205
|
+
const expectedDisplayMode = toSafeString(expected.type).toLowerCase()
|
|
206
|
+
|
|
207
|
+
if (!SUPPORTED_DISPLAY_MODES.includes(displayMode)) {
|
|
208
|
+
return buildInvalidResult(
|
|
209
|
+
'PRODUCT_PLACEMENT_UNSUPPORTED_DISPLAY_MODE',
|
|
210
|
+
`不支持的产品投放位展示形态:${displayMode || 'empty'}`
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (expectedDisplayMode && !SUPPORTED_DISPLAY_MODES.includes(expectedDisplayMode)) {
|
|
215
|
+
return buildInvalidResult(
|
|
216
|
+
'PRODUCT_PLACEMENT_UNSUPPORTED_TYPE',
|
|
217
|
+
`不支持的产品投放位预期展示形态:${expectedDisplayMode}`
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (expectedDisplayMode && displayMode !== expectedDisplayMode) {
|
|
222
|
+
return buildInvalidResult(
|
|
223
|
+
'PRODUCT_PLACEMENT_DISPLAY_MODE_MISMATCH',
|
|
224
|
+
`产品投放位展示形态不匹配:预期 ${expectedDisplayMode},实际 ${displayMode}`
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const configId = String(config.configId === undefined ? '' : config.configId).trim()
|
|
229
|
+
const configAdId = String(config.adId === undefined ? '' : config.adId).trim()
|
|
230
|
+
const expectedAdId = String(expected.adId === undefined ? '' : expected.adId).trim()
|
|
231
|
+
const placementId = toSafeString(config.placementId)
|
|
232
|
+
const productCode = toSafeString(config.productCode)
|
|
233
|
+
const sceneCode = toSafeString(config.sceneCode)
|
|
234
|
+
|
|
235
|
+
if (
|
|
236
|
+
expectedAdId &&
|
|
237
|
+
configAdId !== expectedAdId &&
|
|
238
|
+
configId !== expectedAdId
|
|
239
|
+
) {
|
|
240
|
+
return buildInvalidResult(
|
|
241
|
+
'PRODUCT_CONFIG_ID_MISMATCH',
|
|
242
|
+
'广告 ID 与接口返回的产品 UI 配置不匹配'
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const systemType = toSafeString(config.systemType).toLowerCase()
|
|
247
|
+
|
|
248
|
+
if (!['lyy', 'third_party'].includes(systemType)) {
|
|
249
|
+
return buildInvalidResult(
|
|
250
|
+
'PRODUCT_PLACEMENT_INVALID_SYSTEM_TYPE',
|
|
251
|
+
`不支持的产品系统类型:${systemType || 'empty'}`
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const creative = normalizeCreative(config.creative, displayMode)
|
|
256
|
+
|
|
257
|
+
if (!hasRenderableCreative(creative)) {
|
|
258
|
+
return buildInvalidResult(
|
|
259
|
+
'PRODUCT_PLACEMENT_EMPTY_CREATIVE',
|
|
260
|
+
'产品投放位没有可展示的素材或文案'
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const action = normalizeAction(config.action)
|
|
265
|
+
const normalizedConfig = {
|
|
266
|
+
schemaVersion: Number(config.schemaVersion) || 1,
|
|
267
|
+
adId: configAdId || expectedAdId || configId,
|
|
268
|
+
configId: config.configId === undefined ? '' : config.configId,
|
|
269
|
+
placementId,
|
|
270
|
+
productCode,
|
|
271
|
+
systemType,
|
|
272
|
+
providerCode: toSafeString(config.providerCode),
|
|
273
|
+
channelCode: toSafeString(config.channelCode),
|
|
274
|
+
sceneCode,
|
|
275
|
+
displayMode,
|
|
276
|
+
creative,
|
|
277
|
+
panelStyle: normalizePanelStyle(config.panelStyle),
|
|
278
|
+
action,
|
|
279
|
+
actionable: action.type !== 'none'
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
valid: true,
|
|
284
|
+
config: normalizedConfig,
|
|
285
|
+
errCode: '',
|
|
286
|
+
errMsg: '',
|
|
287
|
+
silent: false
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
module.exports = {
|
|
292
|
+
SUPPORTED_DISPLAY_MODES,
|
|
293
|
+
normalizeActionParams,
|
|
294
|
+
normalizeProductPlacementConfig
|
|
295
|
+
}
|