cardbuddy 0.23.0 → 0.24.0
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/dist/preview-editor/assets/{index-C7N5n_qW.js → index-BumNBo-n.js} +66 -65
- package/dist/preview-editor/assets/{index-7RzNd7tf.css → index-MWKPP9nd.css} +1 -1
- package/dist/preview-editor/index.html +2 -2
- package/dist/vendored/server/core/cardCompile.js +202 -0
- package/dist/vendored/server/core/flameCompile/client.js +466 -0
- package/dist/vendored/server/core/flameCompile/clientElements.js +372 -0
- package/dist/vendored/server/core/flameCompile/common.js +253 -0
- package/dist/vendored/server/core/flameCompile/h5.js +326 -0
- package/dist/vendored/server/core/flameCompile/index.js +126 -0
- package/dist/vendored/server/core/flameCompile/model.js +162 -0
- package/dist/vendored/server/core/index.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 客户端(GaiaX / pmb-ios-flame)元素映射与样式转换
|
|
3
|
+
*
|
|
4
|
+
* 1:1 迁移自 flame-release-mange(Java):
|
|
5
|
+
* client/convert/element/*.java → ELEMENTS 映射表
|
|
6
|
+
* client/convert/element/utils/StyleConstTransformer → styleConst
|
|
7
|
+
* client/convert/element/utils/StyleVarTransformer → styleVar
|
|
8
|
+
*
|
|
9
|
+
* 与 Java 版的差异(均为防御性收敛,不改变正常输入的产物):
|
|
10
|
+
* 1. 未知 className 兜底为 `view`(Java 侧 ElementFactory 返回 null 会 NPE);
|
|
11
|
+
* 2. 圆角/图片裁剪等分组在值为空时跳过(Java 侧无条件 put,会写出 `border-radius:null`)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { hasText } from './common.js'
|
|
15
|
+
|
|
16
|
+
/** 尺寸:数字补 px,`auto` / 百分号 / 空值原样 */
|
|
17
|
+
export function doSize(num) {
|
|
18
|
+
if (num === null || num === undefined) return num
|
|
19
|
+
const s = String(num).trim()
|
|
20
|
+
if (s === '') return num
|
|
21
|
+
if (s === 'auto' || s.includes('%')) return s
|
|
22
|
+
return `${s}px`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function doColor(color) {
|
|
26
|
+
return color
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 颜色/尺寸之外的直通字段 */
|
|
30
|
+
const raw = (v) => v
|
|
31
|
+
|
|
32
|
+
/* ----------------------------- 元素映射表 ------------------------------ */
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} ElementDesc
|
|
36
|
+
* @property {string} type GaiaX 节点类型
|
|
37
|
+
* @property {boolean} [isNewTemplate] 是否拆分独立子模板(列表/网格)
|
|
38
|
+
* @property {Object} [styleDefault] 样式默认值(先于用户样式写入)
|
|
39
|
+
* @property {string[]} [styleConstAppend] 追加在用户样式之后的强制规则(Root 的尺寸兜底)
|
|
40
|
+
* @property {Function} [newTemplatePropsConst] 子模板顶层 const 属性改写
|
|
41
|
+
* @property {Function} [itemStyleConst] 子模板(_item)的 css 规则
|
|
42
|
+
* @property {boolean} [twoWayBinding] 是否需要 value 双向绑定事件
|
|
43
|
+
*/
|
|
44
|
+
export const ELEMENTS = {
|
|
45
|
+
Root: {
|
|
46
|
+
type: 'view',
|
|
47
|
+
styleConstAppend: [
|
|
48
|
+
// 注意:这 5 条在 Java 侧(RootTransfer)是硬编码字面量,冒号后**带空格**,
|
|
49
|
+
// 与 StyleConstTransformer 产出的 `key:value` 拼接口径不同 —— 平台产物天然混用两种写法。
|
|
50
|
+
// 这里照抄原文,否则生成的 index.css 与线上产物字节不一致、对拍必然报差异。
|
|
51
|
+
'width: 100%',
|
|
52
|
+
'flex-direction: column',
|
|
53
|
+
'color: #000000',
|
|
54
|
+
'font-size: 14px',
|
|
55
|
+
'line-height: 150%',
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
Text: { type: 'text' },
|
|
59
|
+
MultiText: { type: 'multitext' },
|
|
60
|
+
Image: { type: 'image' },
|
|
61
|
+
Divider: { type: 'view' },
|
|
62
|
+
Input: { type: 'input', twoWayBinding: true },
|
|
63
|
+
Button: { type: 'button' },
|
|
64
|
+
Icon: { type: 'icon' },
|
|
65
|
+
Map: { type: 'map' },
|
|
66
|
+
Row: { type: 'view' },
|
|
67
|
+
Column: { type: 'view', styleDefault: { 'flex-direction': 'column' } },
|
|
68
|
+
SingleColumn: { type: 'view' },
|
|
69
|
+
MultiColumn: { type: 'view' },
|
|
70
|
+
ListLayout: {
|
|
71
|
+
type: 'grid',
|
|
72
|
+
isNewTemplate: true,
|
|
73
|
+
// 列表默认单列(平台侧同款:column 恒为 1)
|
|
74
|
+
newTemplatePropsConst: (props) => ({ ...props, column: 1 }),
|
|
75
|
+
itemStyleConst: (constProps) =>
|
|
76
|
+
constProps && constProps.direction === 'horizontal' ? ['width: auto'] : ['width: 100%'],
|
|
77
|
+
},
|
|
78
|
+
GridLayout: {
|
|
79
|
+
type: 'grid',
|
|
80
|
+
isNewTemplate: true,
|
|
81
|
+
itemStyleConst: () => ['width: 100%'],
|
|
82
|
+
},
|
|
83
|
+
AbsoluteLayout: { type: 'absolute' },
|
|
84
|
+
ScrollLayout: { type: 'scroll' },
|
|
85
|
+
Canvas: { type: 'canvas' },
|
|
86
|
+
CheckGroup: { type: 'checkgroup', twoWayBinding: true },
|
|
87
|
+
RadioButton: { type: 'radiobutton', twoWayBinding: true },
|
|
88
|
+
ProgressSlider: { type: 'progressslider', twoWayBinding: true },
|
|
89
|
+
Video: { type: 'video' },
|
|
90
|
+
ViewContainer: { type: 'viewcontainer' },
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function elementOf(className) {
|
|
94
|
+
return ELEMENTS[className] || { type: 'view', unknown: true }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ------------------------------ 静态样式 ------------------------------- */
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 样式分组 → CSS 规则数组(`prop:value`)
|
|
101
|
+
* @param {string} className 组件类型(决定是否有额外默认规则)
|
|
102
|
+
* @param {Object} props 样式 const 域(分组对象)
|
|
103
|
+
*/
|
|
104
|
+
export function styleConst(className, props = {}) {
|
|
105
|
+
const out = {}
|
|
106
|
+
const desc = elementOf(className)
|
|
107
|
+
|
|
108
|
+
// 组件级默认(如 Column 的纵向排列)
|
|
109
|
+
if (desc.styleDefault) Object.assign(out, desc.styleDefault)
|
|
110
|
+
|
|
111
|
+
// 平台默认:不放大、可压缩
|
|
112
|
+
out['flex-grow'] = '0'
|
|
113
|
+
out['flex-shrink'] = '1'
|
|
114
|
+
|
|
115
|
+
parseFont(props.font, out)
|
|
116
|
+
parseImage(props.image, out)
|
|
117
|
+
parsePosition(props.position, out)
|
|
118
|
+
parseRect(props.rect, out)
|
|
119
|
+
parseFlex(props.flex, out)
|
|
120
|
+
parseBackground(props.background, out)
|
|
121
|
+
parsePadding(props.padding, out)
|
|
122
|
+
parseMargin(props.margin, out)
|
|
123
|
+
parseBorderRadius(props.borderRadius, out)
|
|
124
|
+
parseBorder(props.border, out)
|
|
125
|
+
parseShadow(props.shadow, out)
|
|
126
|
+
parseTransform(props.transform, out)
|
|
127
|
+
parseOther(props.other, out)
|
|
128
|
+
|
|
129
|
+
const list = Object.entries(out).map(([k, v]) => `${k}:${v}`)
|
|
130
|
+
// 追加规则必须排在最后,才能覆盖上面按 schema 写出的同名声明
|
|
131
|
+
if (desc.styleConstAppend) list.push(...desc.styleConstAppend)
|
|
132
|
+
return list
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseFont(p, out) {
|
|
136
|
+
if (!p) return
|
|
137
|
+
out.color = '#000'
|
|
138
|
+
out['font-size'] = doSize('14')
|
|
139
|
+
out['line-height'] = '150%'
|
|
140
|
+
|
|
141
|
+
if (hasText(p.color)) out.color = doColor(p.color)
|
|
142
|
+
if (hasText(p.fontSize)) out['font-size'] = doSize(p.fontSize)
|
|
143
|
+
if (hasText(p.lineHeight)) out['line-height'] = doSize(p.lineHeight)
|
|
144
|
+
if (hasText(p.textAlign)) out['text-align'] = p.textAlign
|
|
145
|
+
if (hasText(p.fontWeight)) out['font-weight'] = p.fontWeight
|
|
146
|
+
if (hasText(p.fontStyle)) out['font-style'] = p.fontStyle
|
|
147
|
+
if (hasText(p.textDecoration)) out['text-decoration'] = p.textDecoration
|
|
148
|
+
if (String(p.isMaxLine) === '1') {
|
|
149
|
+
out['max-lines'] = hasText(p.maxLine) ? p.maxLine : '1'
|
|
150
|
+
out['text-overflow'] = 'ellipsis'
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseImage(p, out) {
|
|
155
|
+
if (!p) return
|
|
156
|
+
// 只有 Image 组件的 meta 会带 image 分组,这里保持平台侧「裁剪模式二选一」语义
|
|
157
|
+
if (p.cropMode === '01') {
|
|
158
|
+
out.mode = 'cover'
|
|
159
|
+
out['aspect-ratio'] = '1/1'
|
|
160
|
+
} else {
|
|
161
|
+
out.mode = 'contain'
|
|
162
|
+
out['aspect-ratio'] = '16/9'
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parsePosition(p, out) {
|
|
167
|
+
if (!p) return
|
|
168
|
+
for (const k of ['left', 'top', 'right', 'bottom']) {
|
|
169
|
+
if (hasText(p[k])) out[k] = doSize(p[k])
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseRect(p, out) {
|
|
174
|
+
if (!p) return
|
|
175
|
+
const map = {
|
|
176
|
+
width: 'width',
|
|
177
|
+
height: 'height',
|
|
178
|
+
maxWidth: 'max-width',
|
|
179
|
+
maxHeight: 'max-height',
|
|
180
|
+
minWidth: 'min-width',
|
|
181
|
+
minHeight: 'min-height',
|
|
182
|
+
}
|
|
183
|
+
for (const [src, dest] of Object.entries(map)) {
|
|
184
|
+
if (hasText(p[src])) out[dest] = doSize(p[src])
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseFlex(p, out) {
|
|
189
|
+
if (!p) return
|
|
190
|
+
if (hasText(p.flexDirection)) out['flex-direction'] = p.flexDirection
|
|
191
|
+
if (hasText(p.justifyContent)) out['justify-content'] = p.justifyContent
|
|
192
|
+
if (hasText(p.alignItems)) out['align-items'] = p.alignItems
|
|
193
|
+
if (hasText(p.flexWrap)) out['flex-wrap'] = p.flexWrap
|
|
194
|
+
if (hasText(p.flexShrink)) out['flex-shrink'] = p.flexShrink
|
|
195
|
+
if (hasText(p.gap)) out.gap = doSize(p.gap)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parseBackground(p, out) {
|
|
199
|
+
if (!p) return
|
|
200
|
+
if (p.backgroundType === '01') {
|
|
201
|
+
if (hasText(p.backgroundColor)) out['background-color'] = p.backgroundColor
|
|
202
|
+
} else if (p.backgroundType === '02') {
|
|
203
|
+
if (hasText(p.backgroundImage)) out['background-image'] = `url('${p.backgroundImage}')`
|
|
204
|
+
out['background-size'] = hasText(p.backgroundSize) ? p.backgroundSize : 'cover'
|
|
205
|
+
out['background-position'] = hasText(p.backgroundPosition) ? p.backgroundPosition : '50% 50%'
|
|
206
|
+
out['background-repeat'] = hasText(p.backgroundRepeat) ? p.backgroundRepeat : 'no-repeat'
|
|
207
|
+
} else if (p.backgroundType === '03') {
|
|
208
|
+
if (hasText(p.backgroundGradient)) out['background-image'] = p.backgroundGradient
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function parsePadding(p, out) {
|
|
213
|
+
if (!p) return
|
|
214
|
+
if (p.paddingType === '0') {
|
|
215
|
+
if (hasText(p.padding)) {
|
|
216
|
+
const v = doSize(p.padding)
|
|
217
|
+
out['padding-left'] = v
|
|
218
|
+
out['padding-top'] = v
|
|
219
|
+
out['padding-right'] = v
|
|
220
|
+
out['padding-bottom'] = v
|
|
221
|
+
}
|
|
222
|
+
} else if (p.paddingType === '1') {
|
|
223
|
+
for (const side of ['left', 'top', 'right', 'bottom']) {
|
|
224
|
+
const key = `padding${side[0].toUpperCase()}${side.slice(1)}`
|
|
225
|
+
if (hasText(p[key])) out[`padding-${side}`] = doSize(p[key])
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseMargin(p, out) {
|
|
231
|
+
if (!p) return
|
|
232
|
+
if (p.marginType === '0') {
|
|
233
|
+
if (hasText(p.margin)) {
|
|
234
|
+
const v = doSize(p.margin)
|
|
235
|
+
out['margin-left'] = v
|
|
236
|
+
out['margin-top'] = v
|
|
237
|
+
out['margin-right'] = v
|
|
238
|
+
out['margin-bottom'] = v
|
|
239
|
+
}
|
|
240
|
+
} else if (p.marginType === '1') {
|
|
241
|
+
for (const side of ['left', 'top', 'right', 'bottom']) {
|
|
242
|
+
const key = `margin${side[0].toUpperCase()}${side.slice(1)}`
|
|
243
|
+
if (hasText(p[key])) out[`margin-${side}`] = doSize(p[key])
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parseBorderRadius(p, out) {
|
|
249
|
+
if (!p) return
|
|
250
|
+
if (p.borderRadiusType === '0') {
|
|
251
|
+
if (hasText(p.borderRadius)) out['border-radius'] = doSize(p.borderRadius)
|
|
252
|
+
} else if (p.borderRadiusType === '1') {
|
|
253
|
+
const map = {
|
|
254
|
+
borderTopLeftRadius: 'border-top-left-radius',
|
|
255
|
+
borderTopRightRadius: 'border-top-right-radius',
|
|
256
|
+
borderBottomRightRadius: 'border-bottom-right-radius',
|
|
257
|
+
borderBottomLeftRadius: 'border-bottom-left-radius',
|
|
258
|
+
}
|
|
259
|
+
for (const [src, dest] of Object.entries(map)) {
|
|
260
|
+
if (hasText(p[src])) out[dest] = doSize(p[src])
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function parseBorder(p, out) {
|
|
266
|
+
if (!p) return
|
|
267
|
+
if (String(p.isBorder) !== '1') return
|
|
268
|
+
out['border-width'] = doSize(hasText(p.borderWidth) ? p.borderWidth : '1')
|
|
269
|
+
out['border-style'] = hasText(p.borderStyle) ? p.borderStyle : 'solid'
|
|
270
|
+
out['border-color'] = doColor(hasText(p.borderColor) ? p.borderColor : '#e7e7ee')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function parseShadow(p, out) {
|
|
274
|
+
if (!p) return
|
|
275
|
+
if (String(p.isShadow) !== '1') return
|
|
276
|
+
const parts = [doSize(hasText(p.shadowX) ? p.shadowX : '0'), doSize(hasText(p.shadowY) ? p.shadowY : '0')]
|
|
277
|
+
if (hasText(p.shadowBlur)) parts.push(doSize(p.shadowBlur))
|
|
278
|
+
if (hasText(p.shadowSpread)) parts.push(doSize(p.shadowSpread))
|
|
279
|
+
parts.push(doColor(hasText(p.shadowColor) ? p.shadowColor : '#e7e7ee'))
|
|
280
|
+
out['box-shadow'] = parts.join(' ')
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function parseTransform(p, out) {
|
|
284
|
+
if (!p) return
|
|
285
|
+
if (hasText(p.rotate)) out.transform = `rotate(${p.rotate}deg)`
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function parseOther(p, out) {
|
|
289
|
+
if (!p) return
|
|
290
|
+
if (hasText(p.overflow) && p.overflow !== 'hidden') out.overflow = p.overflow
|
|
291
|
+
if (hasText(p.opacity)) out.opacity = (Number(p.opacity) / 100).toFixed(2)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* ------------------------------ 动态样式 ------------------------------- */
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 样式分组(动态域)→ databinding 的 `extend` 映射
|
|
298
|
+
* 与静态样式的差异:值本身已是表达式,不补 px、不填默认值;
|
|
299
|
+
* shadow / transform / position / opacity 平台侧不导出,这里保持一致。
|
|
300
|
+
*/
|
|
301
|
+
export function styleVar(props = {}) {
|
|
302
|
+
const out = {}
|
|
303
|
+
const font = props.font || {}
|
|
304
|
+
if (hasText(font.color)) out.color = raw(font.color)
|
|
305
|
+
if (hasText(font.fontSize)) out['font-size'] = raw(font.fontSize)
|
|
306
|
+
if (hasText(font.lineHeight)) out['line-height'] = raw(font.lineHeight)
|
|
307
|
+
if (hasText(font.textAlign)) out['text-align'] = raw(font.textAlign)
|
|
308
|
+
if (hasText(font.fontWeight)) out['font-weight'] = raw(font.fontWeight)
|
|
309
|
+
if (hasText(font.fontStyle)) out['font-style'] = raw(font.fontStyle)
|
|
310
|
+
if (hasText(font.textDecoration)) out['text-decoration'] = raw(font.textDecoration)
|
|
311
|
+
if (hasText(font.maxLine)) out['max-lines'] = raw(font.maxLine)
|
|
312
|
+
|
|
313
|
+
const rect = props.rect || {}
|
|
314
|
+
const rectMap = {
|
|
315
|
+
width: 'width',
|
|
316
|
+
height: 'height',
|
|
317
|
+
maxWidth: 'max-width',
|
|
318
|
+
maxHeight: 'max-height',
|
|
319
|
+
minWidth: 'min-width',
|
|
320
|
+
minHeight: 'min-height',
|
|
321
|
+
}
|
|
322
|
+
for (const [src, dest] of Object.entries(rectMap)) {
|
|
323
|
+
if (hasText(rect[src])) out[dest] = raw(rect[src])
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const flex = props.flex || {}
|
|
327
|
+
if (hasText(flex.flexDirection)) out['flex-direction'] = raw(flex.flexDirection)
|
|
328
|
+
if (hasText(flex.justifyContent)) out['justify-content'] = raw(flex.justifyContent)
|
|
329
|
+
if (hasText(flex.alignItems)) out['align-items'] = raw(flex.alignItems)
|
|
330
|
+
if (hasText(flex.flexWrap)) out['flex-wrap'] = raw(flex.flexWrap)
|
|
331
|
+
if (hasText(flex.gap)) out.gap = raw(flex.gap)
|
|
332
|
+
|
|
333
|
+
const bg = props.background || {}
|
|
334
|
+
if (hasText(bg.backgroundColor)) out['background-color'] = raw(bg.backgroundColor)
|
|
335
|
+
if (hasText(bg.backgroundImage)) out['background-image'] = `'url('+(${bg.backgroundImage})+')'`
|
|
336
|
+
if (hasText(bg.backgroundSize)) out['background-size'] = raw(bg.backgroundSize)
|
|
337
|
+
if (hasText(bg.backgroundPosition)) out['background-position'] = raw(bg.backgroundPosition)
|
|
338
|
+
if (hasText(bg.backgroundRepeat)) out['background-repeat'] = raw(bg.backgroundRepeat)
|
|
339
|
+
if (!hasText(bg.backgroundImage) && hasText(bg.backgroundGradient)) {
|
|
340
|
+
out['background-image'] = raw(bg.backgroundGradient)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const padding = props.padding || {}
|
|
344
|
+
if (hasText(padding.padding)) out.padding = raw(padding.padding)
|
|
345
|
+
for (const side of ['left', 'top', 'right', 'bottom']) {
|
|
346
|
+
const key = `padding${side[0].toUpperCase()}${side.slice(1)}`
|
|
347
|
+
if (hasText(padding[key])) out[`padding-${side}`] = raw(padding[key])
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const margin = props.margin || {}
|
|
351
|
+
if (hasText(margin.margin)) out.margin = raw(margin.margin)
|
|
352
|
+
for (const side of ['left', 'top', 'right', 'bottom']) {
|
|
353
|
+
const key = `margin${side[0].toUpperCase()}${side.slice(1)}`
|
|
354
|
+
if (hasText(margin[key])) out[`margin-${side}`] = raw(margin[key])
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const radius = props.borderRadius || {}
|
|
358
|
+
if (hasText(radius.borderRadius)) out['border-radius'] = raw(radius.borderRadius)
|
|
359
|
+
for (const corner of ['TopLeft', 'TopRight', 'BottomRight', 'BottomLeft']) {
|
|
360
|
+
const key = `border${corner}Radius`
|
|
361
|
+
if (hasText(radius[key])) {
|
|
362
|
+
out[`border-${corner.replace(/([A-Z])/g, '-$1').toLowerCase()}-radius`] = raw(radius[key])
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const border = props.border || {}
|
|
367
|
+
if (hasText(border.borderWidth)) out['border-width'] = raw(border.borderWidth)
|
|
368
|
+
if (hasText(border.borderStyle)) out['border-style'] = raw(border.borderStyle)
|
|
369
|
+
if (hasText(border.borderColor)) out['border-color'] = raw(border.borderColor)
|
|
370
|
+
|
|
371
|
+
return out
|
|
372
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 编译公共层:属性摊平、表达式改写、条件表达式
|
|
3
|
+
*
|
|
4
|
+
* 1:1 迁移自 flame-release-mange(Java):
|
|
5
|
+
* client/convert/PropertyConvert → convertPropsClient
|
|
6
|
+
* h5/convert/PropertyConvert → convertPropsH5(直接产出 JS 字面量)
|
|
7
|
+
* h5/convert/ExpressionConvert → convertExpression
|
|
8
|
+
* client/convert/ConditionConvert → convertCondition
|
|
9
|
+
*
|
|
10
|
+
* 三者的分工是平台侧的既定契约,不要"优化":
|
|
11
|
+
* - 客户端(GaiaX)走 `{const, var}` 双域:const 进 css / index.data,var 进 index.databinding;
|
|
12
|
+
* - H5 走单域 JS 字面量,动态属性在生成代码里拼 `s.$xxx`(s = 卡片上下文 state)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Java StringUtils.hasText 等价 */
|
|
16
|
+
export function hasText(v) {
|
|
17
|
+
return typeof v === 'string' ? v.trim().length > 0 : v !== undefined && v !== null && String(v).trim().length > 0
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 资源地址替换(平台侧当前为恒等,保留钩子便于对接 CDN 前缀) */
|
|
21
|
+
export function replaceUrl(url) {
|
|
22
|
+
return url
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* --------------------------- 客户端:const/var 双域 -------------------------- */
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 属性集 → `{ const, var }`
|
|
29
|
+
* 静态属性进 const(并对动态属性补 itemDefaultValue 作为兜底),动态属性进 var,
|
|
30
|
+
* 资源地址按静态处理(Java 侧同款)。
|
|
31
|
+
*/
|
|
32
|
+
export function convertPropsClient(properties = []) {
|
|
33
|
+
const constProps = {}
|
|
34
|
+
const varProps = {}
|
|
35
|
+
|
|
36
|
+
const walk = (props, constOut, varOut) => {
|
|
37
|
+
for (const prop of props) {
|
|
38
|
+
const key = prop.itemKey
|
|
39
|
+
if (!hasText(key)) continue
|
|
40
|
+
// 缺省按静态处理:平台产出的 schema 一定带 itemValueFrom,
|
|
41
|
+
// 但手工编辑 / AI 生成的数据可能省略该字段,缺省兜底比丢属性安全。
|
|
42
|
+
const from = prop.itemValueFrom || '0'
|
|
43
|
+
|
|
44
|
+
if (from === '0') {
|
|
45
|
+
if (prop.children && prop.children.length > 0) {
|
|
46
|
+
const groupConst = {}
|
|
47
|
+
const groupVar = {}
|
|
48
|
+
walk(prop.children, groupConst, groupVar)
|
|
49
|
+
constOut[key] = groupConst
|
|
50
|
+
varOut[key] = groupVar
|
|
51
|
+
} else if (prop.list && prop.list.length > 0) {
|
|
52
|
+
const listConst = []
|
|
53
|
+
const listVar = []
|
|
54
|
+
for (const group of prop.list) {
|
|
55
|
+
const groupConst = {}
|
|
56
|
+
const groupVar = {}
|
|
57
|
+
walk(group, groupConst, groupVar)
|
|
58
|
+
listConst.push(groupConst)
|
|
59
|
+
listVar.push(groupVar)
|
|
60
|
+
}
|
|
61
|
+
constOut[key] = listConst
|
|
62
|
+
varOut[key] = listVar
|
|
63
|
+
} else {
|
|
64
|
+
constOut[key] = prop.itemValue
|
|
65
|
+
}
|
|
66
|
+
} else if (from === '1') {
|
|
67
|
+
// 动态属性:const 用默认值兜底,var 存表达式
|
|
68
|
+
constOut[key] = prop.itemDefaultValue
|
|
69
|
+
varOut[key] = prop.itemValue
|
|
70
|
+
} else if (from === '2') {
|
|
71
|
+
constOut[key] = replaceUrl(prop.itemValue)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
walk(properties, constProps, varProps)
|
|
77
|
+
return { const: constProps, var: varProps }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 取动态属性(表达式)映射,供双向绑定事件生成 */
|
|
81
|
+
export function pickVarProps(properties = []) {
|
|
82
|
+
const out = {}
|
|
83
|
+
for (const p of properties) {
|
|
84
|
+
if (p.itemValueFrom === '1' && hasText(p.itemValue)) out[p.itemKey] = p.itemValue
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 属性集 → 普通对象(平台 `PropertyConvert.convertProps(List)` 的口径)
|
|
91
|
+
*
|
|
92
|
+
* 与 `convertPropsClient` 的区别:动态属性的值**原样保留**(`$keyword` 就是 `$keyword`),
|
|
93
|
+
* 不换成默认值、也不改写成 `s.$keyword`。H5 的 JSAPI 事件参数走这条路 ——
|
|
94
|
+
* 平台侧把它交给 JSON 序列化,于是 `$x` 落进 JSON 字符串里由宿主解析,
|
|
95
|
+
* 若在这里提前改写,参数就从「宿主可识别的占位」变成了「本地变量引用」,行为与线上不同。
|
|
96
|
+
*/
|
|
97
|
+
export function collectProps(properties = []) {
|
|
98
|
+
const out = {}
|
|
99
|
+
for (const prop of properties) {
|
|
100
|
+
if (!hasText(prop.itemKey)) continue
|
|
101
|
+
const from = prop.itemValueFrom || '0'
|
|
102
|
+
if (from === '0') {
|
|
103
|
+
if (prop.children && prop.children.length > 0) out[prop.itemKey] = collectProps(prop.children)
|
|
104
|
+
else if (prop.list && prop.list.length > 0) out[prop.itemKey] = prop.list.map((g) => collectProps(g))
|
|
105
|
+
else out[prop.itemKey] = prop.itemValue
|
|
106
|
+
} else if (from === '1') {
|
|
107
|
+
out[prop.itemKey] = prop.itemValue
|
|
108
|
+
} else if (from === '2') {
|
|
109
|
+
out[prop.itemKey] = replaceUrl(prop.itemValue)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ----------------------------- H5:JS 字面量 ----------------------------- */
|
|
116
|
+
|
|
117
|
+
const IDENT_RE = /^[A-Za-z0-9_$]+$/
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* JS 对象字面量的键名写法(平台口径)
|
|
121
|
+
*
|
|
122
|
+
* Java 侧 `PropertyConvert.convertProps` 的规则是:键名匹配 `^[a-zA-Z0-9_$]+$` 就裸写,
|
|
123
|
+
* 不匹配才走 `JSONObject.toJSONString` 加引号。H5 产物的 style / 事件名 / 插槽名都吃这条规则,
|
|
124
|
+
* 所以统一收敛到这一个函数,避免各处口径漂移。
|
|
125
|
+
*/
|
|
126
|
+
export function jsKey(key) {
|
|
127
|
+
return IDENT_RE.test(key) ? key : JSON.stringify(key)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 属性集 → JS 对象字面量源码(不含外层花括号以外的东西,返回 `{...}`)
|
|
132
|
+
* 注意:静态值一律 JSON.stringify(与 Java 侧 fastjson 口径一致)。
|
|
133
|
+
*/
|
|
134
|
+
export function convertPropsH5(properties = []) {
|
|
135
|
+
const parts = []
|
|
136
|
+
for (const prop of properties) {
|
|
137
|
+
if (!hasText(prop.itemKey)) continue
|
|
138
|
+
const key = IDENT_RE.test(prop.itemKey) ? prop.itemKey : JSON.stringify(prop.itemKey)
|
|
139
|
+
const from = prop.itemValueFrom || '0'
|
|
140
|
+
|
|
141
|
+
if (from === '0') {
|
|
142
|
+
if (prop.children && prop.children.length > 0) {
|
|
143
|
+
parts.push(`${key}:${convertPropsH5(prop.children)}`)
|
|
144
|
+
} else if (prop.list && prop.list.length > 0) {
|
|
145
|
+
const groups = prop.list.map((g) => convertPropsH5(g))
|
|
146
|
+
parts.push(`${key}:[${groups.join(',')}]`)
|
|
147
|
+
} else {
|
|
148
|
+
parts.push(`${key}:${JSON.stringify(prop.itemValue)}`)
|
|
149
|
+
}
|
|
150
|
+
} else if (from === '1') {
|
|
151
|
+
if (hasText(prop.itemValue)) parts.push(`${key}:${convertExpression(prop.itemValue)}`)
|
|
152
|
+
} else if (from === '2') {
|
|
153
|
+
parts.push(`${key}:${JSON.stringify(replaceUrl(prop.itemValue))}`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return `{${parts.join(',')}}`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ------------------------------ 表达式改写 ------------------------------ */
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* `$foo` → `s.$foo`
|
|
163
|
+
*
|
|
164
|
+
* 平台卡片状态挂在上下文实例的 `state` 上(生成代码里 `var s = c.state`),
|
|
165
|
+
* 所以表达式里的 `$x` 必须改写为 `s.$x`。
|
|
166
|
+
* 字符串字面量('…' / "…" / `…`)内部的 `$` 不得改写。
|
|
167
|
+
*
|
|
168
|
+
* 关于 `$$`:Java 版原实现只判断「前一个字符是否为 $」,导致 `$$` 被改写成 `s.$$`
|
|
169
|
+
* (首字符仍被替换,与其注释意图不符)。这里按意图修正为**整体保留** ——
|
|
170
|
+
* `$$` 是平台表达式的内建变量(列表项数据入口),加 `s.` 前缀必然取不到值。
|
|
171
|
+
*/
|
|
172
|
+
export function convertExpression(expression) {
|
|
173
|
+
const text = String(expression ?? '')
|
|
174
|
+
// 先标出所有字符串字面量覆盖的下标
|
|
175
|
+
const stringRanges = new Set()
|
|
176
|
+
const re = /(['"`])(?:\\.|(?!\1).)*?\1/g
|
|
177
|
+
let m
|
|
178
|
+
while ((m = re.exec(text)) !== null) {
|
|
179
|
+
for (let i = m.index; i < m.index + m[0].length; i++) stringRanges.add(i)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let out = ''
|
|
183
|
+
for (let i = 0; i < text.length; i++) {
|
|
184
|
+
const c = text[i]
|
|
185
|
+
if (stringRanges.has(i)) {
|
|
186
|
+
out += c
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
if (c === '$') {
|
|
190
|
+
// `$$` 内建变量整体保留
|
|
191
|
+
if (text[i + 1] === '$') {
|
|
192
|
+
out += '$$'
|
|
193
|
+
i += 1
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
out += 's.$'
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
out += c
|
|
200
|
+
}
|
|
201
|
+
return out
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* ------------------------------ 条件表达式 ------------------------------ */
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 结构化条件(v-show 的 condition.group)→ 布尔表达式
|
|
208
|
+
* 节点形态:`{ logic:'&&'|'||', group:[...] }` 或 `{ logic, condition:{ val1Type,val1Value,operator,val2Type,val2Value } }`
|
|
209
|
+
* val1Type/val2Type:'1' 表示表达式(原样输出),其它表示字面量(JSON 编码)。
|
|
210
|
+
*/
|
|
211
|
+
export function convertCondition(group) {
|
|
212
|
+
const parts = []
|
|
213
|
+
collectGroup(group, parts)
|
|
214
|
+
return parts.join('')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function collectGroup(group, parts) {
|
|
218
|
+
if (!Array.isArray(group) || group.length === 0) return
|
|
219
|
+
const wrapped = group.length > 1
|
|
220
|
+
if (wrapped) parts.push('(')
|
|
221
|
+
group.forEach((item, index) => {
|
|
222
|
+
if (index > 0 && hasText(item.logic)) parts.push(item.logic)
|
|
223
|
+
if (Array.isArray(item.group)) collectGroup(item.group, parts)
|
|
224
|
+
else if (item.condition) collectCondition(item.condition, parts)
|
|
225
|
+
})
|
|
226
|
+
if (wrapped) parts.push(')')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function collectCondition(condition, parts) {
|
|
230
|
+
const left = condition.val1Type === '1' ? condition.val1Value : JSON.stringify(condition.val1Value)
|
|
231
|
+
const right = condition.val2Type === '1' ? condition.val2Value : JSON.stringify(condition.val2Value)
|
|
232
|
+
parts.push('(')
|
|
233
|
+
switch (condition.operator) {
|
|
234
|
+
case '==':
|
|
235
|
+
case '!=':
|
|
236
|
+
case '<':
|
|
237
|
+
case '>':
|
|
238
|
+
case '<=':
|
|
239
|
+
case '>=':
|
|
240
|
+
parts.push(`${left}${condition.operator}${right}`)
|
|
241
|
+
break
|
|
242
|
+
case 'blank':
|
|
243
|
+
parts.push(`!${left}`)
|
|
244
|
+
break
|
|
245
|
+
case 'notblank':
|
|
246
|
+
parts.push(`!!${left}`)
|
|
247
|
+
break
|
|
248
|
+
default:
|
|
249
|
+
// 平台侧同款兜底:未知算子恒真,避免整块被吞掉
|
|
250
|
+
parts.push('1 == 1')
|
|
251
|
+
}
|
|
252
|
+
parts.push(')')
|
|
253
|
+
}
|