km-card-layout-core 0.1.1 → 0.1.3
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/README.md +1 -1
- package/dist/index.js +38 -175
- package/dist/utils.js +67 -10
- package/index.ts +54 -210
- package/interface/data/payload.ts +4 -1
- package/interface/elements.ts +16 -53
- package/interface/render.ts +12 -2
- package/package.json +2 -2
- package/types.d.ts +16 -0
- package/utils.ts +136 -67
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -25,7 +25,6 @@ __exportStar(require("./interface"), exports);
|
|
|
25
25
|
/** ---------- 常量 ---------- */
|
|
26
26
|
const DEFAULT_CARD_WIDTH = 343; // 默认卡片宽度(像素)
|
|
27
27
|
const DEFAULT_CARD_HEIGHT = 210; // 默认卡片高度(像素)
|
|
28
|
-
const DEFAULT_GROUP_GAP = 22; // repeatable-item 默认纵向间距
|
|
29
28
|
const DIMENSION_PROPS = new Set([
|
|
30
29
|
'width',
|
|
31
30
|
'height',
|
|
@@ -51,7 +50,6 @@ const DIMENSION_PROPS = new Set([
|
|
|
51
50
|
'gap',
|
|
52
51
|
'rowGap',
|
|
53
52
|
'columnGap',
|
|
54
|
-
'flexBasis',
|
|
55
53
|
]);
|
|
56
54
|
/** ---------- 基础工具 ---------- */
|
|
57
55
|
/**
|
|
@@ -125,9 +123,6 @@ const isObject = (val) => Boolean(val) && typeof val === 'object';
|
|
|
125
123
|
const getAbsLayout = (el) => el.layout && el.layout.mode === 'absolute'
|
|
126
124
|
? el.layout
|
|
127
125
|
: null;
|
|
128
|
-
const getFlexLayout = (el) => el.layout && el.layout.mode === 'flex'
|
|
129
|
-
? el.layout
|
|
130
|
-
: null;
|
|
131
126
|
/** ---------- 布局与数据解析 ---------- */
|
|
132
127
|
/**
|
|
133
128
|
* 归一化布局输入(对象或 JSON 字符串),补齐宽高/容器/children 默认值。
|
|
@@ -188,30 +183,12 @@ const readByPath = (data, path) => {
|
|
|
188
183
|
return cursor;
|
|
189
184
|
};
|
|
190
185
|
/**
|
|
191
|
-
*
|
|
192
|
-
* - 全局 binding:直接基于根数据;
|
|
193
|
-
* - repeatable-group:binding 等于/前缀 dataPath 或以 `$item.` 开头时,改用当前条目数据。
|
|
186
|
+
* Resolve element binding against provided data.
|
|
194
187
|
*/
|
|
195
188
|
const resolveBindingValue = (binding, rootData, context) => {
|
|
196
189
|
if (!binding)
|
|
197
190
|
return undefined;
|
|
198
|
-
const
|
|
199
|
-
let target = rootData;
|
|
200
|
-
let path = binding;
|
|
201
|
-
// repeatable-group: binding 等于 dataPath 或以其为前缀时,切换到当前条目数据
|
|
202
|
-
if (contextBinding &&
|
|
203
|
-
(binding === contextBinding || binding.startsWith(`${contextBinding}.`))) {
|
|
204
|
-
target = contextData;
|
|
205
|
-
path =
|
|
206
|
-
binding === contextBinding
|
|
207
|
-
? ''
|
|
208
|
-
: binding.slice(contextBinding.length + 1);
|
|
209
|
-
}
|
|
210
|
-
else if (binding.startsWith('$item.')) {
|
|
211
|
-
target = contextData;
|
|
212
|
-
path = binding.slice('$item.'.length);
|
|
213
|
-
}
|
|
214
|
-
const value = readByPath(target, path);
|
|
191
|
+
const value = readByPath(rootData, binding);
|
|
215
192
|
return value === undefined ? undefined : value;
|
|
216
193
|
};
|
|
217
194
|
exports.resolveBindingValue = resolveBindingValue;
|
|
@@ -220,7 +197,7 @@ exports.resolveBindingValue = resolveBindingValue;
|
|
|
220
197
|
* 生成元素外层样式(绝对/弹性布局),始终返回内联样式字符串。
|
|
221
198
|
*/
|
|
222
199
|
const buildWrapperStyle = (el, unit) => {
|
|
223
|
-
var _a
|
|
200
|
+
var _a;
|
|
224
201
|
const abs = getAbsLayout(el);
|
|
225
202
|
if (abs) {
|
|
226
203
|
const textAlign = (_a = el === null || el === void 0 ? void 0 : el.style) === null || _a === void 0 ? void 0 : _a.textAlign;
|
|
@@ -235,65 +212,18 @@ const buildWrapperStyle = (el, unit) => {
|
|
|
235
212
|
textAlign,
|
|
236
213
|
}, unit);
|
|
237
214
|
}
|
|
238
|
-
|
|
239
|
-
if (!flex)
|
|
240
|
-
return '';
|
|
241
|
-
const item = flex.item || {};
|
|
242
|
-
return (0, exports.styleObjectToString)({
|
|
243
|
-
width: (0, exports.addUnit)(flex.width, unit),
|
|
244
|
-
height: (0, exports.addUnit)(flex.height, unit),
|
|
245
|
-
order: item.order,
|
|
246
|
-
flexGrow: item.flexGrow,
|
|
247
|
-
flexShrink: item.flexShrink,
|
|
248
|
-
flexBasis: item.flexBasis,
|
|
249
|
-
alignSelf: item.alignSelf,
|
|
250
|
-
boxSizing: 'border-box',
|
|
251
|
-
textAlign: (_b = el === null || el === void 0 ? void 0 : el.style) === null || _b === void 0 ? void 0 : _b.textAlign,
|
|
252
|
-
}, unit);
|
|
215
|
+
return '';
|
|
253
216
|
};
|
|
254
217
|
/**
|
|
255
218
|
* padding 数组/数字转 CSS 缩写字符串。
|
|
256
219
|
*/
|
|
257
|
-
const formatPadding = (padding, unit) => {
|
|
258
|
-
if (padding === undefined || padding === null)
|
|
259
|
-
return undefined;
|
|
260
|
-
if (typeof padding === 'number')
|
|
261
|
-
return (0, exports.addUnit)(padding, unit);
|
|
262
|
-
if (Array.isArray(padding)) {
|
|
263
|
-
const list = padding
|
|
264
|
-
.filter(v => v !== undefined && v !== null)
|
|
265
|
-
.map(v => (0, exports.addUnit)(v, unit));
|
|
266
|
-
if (!list.length)
|
|
267
|
-
return undefined;
|
|
268
|
-
if (list.length === 2)
|
|
269
|
-
return `${list[0]} ${list[1]}`;
|
|
270
|
-
if (list.length === 3)
|
|
271
|
-
return `${list[0]} ${list[1]} ${list[2]}`;
|
|
272
|
-
if (list.length >= 4)
|
|
273
|
-
return `${list[0]} ${list[1]} ${list[2]} ${list[3]}`;
|
|
274
|
-
}
|
|
275
|
-
return undefined;
|
|
276
|
-
};
|
|
277
220
|
/**
|
|
278
|
-
* 构建 layout-panel
|
|
221
|
+
* 构建 layout-panel 的容器样式(绝对布局容器)。
|
|
279
222
|
*/
|
|
280
223
|
const buildPanelStyle = (el, unit) => {
|
|
281
|
-
const options = (el.container && el.container.options) || {};
|
|
282
224
|
const style = {
|
|
283
|
-
display: '
|
|
284
|
-
flexDirection: options.direction || 'row',
|
|
285
|
-
flexWrap: options.wrap || 'nowrap',
|
|
286
|
-
justifyContent: options.justifyContent,
|
|
287
|
-
alignItems: options.alignItems,
|
|
288
|
-
padding: formatPadding(options.padding, unit),
|
|
225
|
+
display: 'block',
|
|
289
226
|
};
|
|
290
|
-
if (options.gap && typeof options.gap === 'number') {
|
|
291
|
-
style.gap = (0, exports.addUnit)(options.gap, unit);
|
|
292
|
-
}
|
|
293
|
-
else if (options.gap && isObject(options.gap)) {
|
|
294
|
-
style.rowGap = (0, exports.addUnit)(options.gap.row, unit);
|
|
295
|
-
style.columnGap = (0, exports.addUnit)(options.gap.column, unit);
|
|
296
|
-
}
|
|
297
227
|
return (0, exports.styleObjectToString)(style, unit);
|
|
298
228
|
};
|
|
299
229
|
/**
|
|
@@ -304,106 +234,47 @@ const normalizeElementStyle = (style, unit) => {
|
|
|
304
234
|
return '';
|
|
305
235
|
return (0, exports.styleObjectToString)(style, unit);
|
|
306
236
|
};
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
(group.itemTemplate || []).length;
|
|
337
|
-
const template = (group.items && group.items[0] && group.items[0].elements) ||
|
|
338
|
-
group.itemTemplate ||
|
|
339
|
-
[];
|
|
340
|
-
const gap = inferGroupGap(group);
|
|
341
|
-
// Use saved items (from designer) first
|
|
342
|
-
if (group.items && group.items.length) {
|
|
343
|
-
group.items.slice(0, maxItems).forEach((item, idx) => {
|
|
344
|
-
const payload = dataList[idx] !== undefined ? dataList[idx] : item.data;
|
|
345
|
-
(item.elements || []).forEach(el => {
|
|
346
|
-
result.push({
|
|
347
|
-
element: el,
|
|
348
|
-
contextData: payload,
|
|
349
|
-
contextBinding: group.dataPath,
|
|
350
|
-
});
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
return result;
|
|
354
|
-
}
|
|
355
|
-
// Otherwise clone from template by gap
|
|
356
|
-
dataList.slice(0, maxItems).forEach((payload, idx) => {
|
|
357
|
-
template.forEach(el => {
|
|
358
|
-
const abs = getAbsLayout(el);
|
|
359
|
-
const cloned = abs
|
|
360
|
-
? {
|
|
361
|
-
...el,
|
|
362
|
-
layout: { ...abs, y: abs.y + idx * gap },
|
|
363
|
-
}
|
|
364
|
-
: el;
|
|
365
|
-
result.push({
|
|
366
|
-
element: cloned,
|
|
367
|
-
contextData: payload,
|
|
368
|
-
contextBinding: group.dataPath,
|
|
369
|
-
});
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
return result;
|
|
237
|
+
const buildTextIcon = (el, unit) => {
|
|
238
|
+
var _a, _b, _c;
|
|
239
|
+
const icon = el.icon;
|
|
240
|
+
if (!icon || icon.enable === false)
|
|
241
|
+
return undefined;
|
|
242
|
+
const style = icon.style || 'fill';
|
|
243
|
+
const baseName = el.key || el.binding || el.id;
|
|
244
|
+
let name;
|
|
245
|
+
if (style === 'dot')
|
|
246
|
+
name = 'round';
|
|
247
|
+
else if (style === 'line')
|
|
248
|
+
name = baseName ? `${baseName}-line` : undefined;
|
|
249
|
+
else
|
|
250
|
+
name = baseName || undefined;
|
|
251
|
+
if (!name)
|
|
252
|
+
return undefined;
|
|
253
|
+
const size = icon.size !== undefined && icon.size !== null
|
|
254
|
+
? icon.size
|
|
255
|
+
: (_a = el.style) === null || _a === void 0 ? void 0 : _a.fontSize;
|
|
256
|
+
const gap = icon.gap !== undefined && icon.gap !== null ? icon.gap : 4;
|
|
257
|
+
const color = (_b = icon.color) !== null && _b !== void 0 ? _b : (typeof ((_c = el.style) === null || _c === void 0 ? void 0 : _c.color) === 'string' ? el.style.color : undefined);
|
|
258
|
+
return {
|
|
259
|
+
name: `${name}`,
|
|
260
|
+
text: `${name}`,
|
|
261
|
+
size: (0, exports.addUnit)(size, unit),
|
|
262
|
+
gap: (0, exports.addUnit)(gap, unit),
|
|
263
|
+
color: color,
|
|
264
|
+
align: icon.align || 'left',
|
|
265
|
+
};
|
|
373
266
|
};
|
|
374
267
|
/** ---------- 渲染树构建 ---------- */
|
|
375
268
|
/**
|
|
376
|
-
* 将 children
|
|
269
|
+
* 将 children 展开为渲染节点。
|
|
377
270
|
*/
|
|
378
271
|
const buildRenderNodes = (children, rootData, unit = 'px', context = {}) => {
|
|
379
272
|
if (!Array.isArray(children))
|
|
380
273
|
return [];
|
|
381
|
-
// Mark mutually exclusive bindings to hide them when repeatable-group is present
|
|
382
|
-
const excluded = new Set();
|
|
383
|
-
children.forEach(el => {
|
|
384
|
-
if (el && el.type === 'repeatable-group' && !!el.visible) {
|
|
385
|
-
(el.mutualExcludes || []).forEach(b => excluded.add(b));
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
274
|
const nodes = [];
|
|
389
275
|
children.forEach(el => {
|
|
390
276
|
if (!el || el.visible === false)
|
|
391
277
|
return;
|
|
392
|
-
if (el.type === 'repeatable-group') {
|
|
393
|
-
const instances = materializeRepeatableItems(el, rootData);
|
|
394
|
-
instances.forEach(({ element, contextData, contextBinding }) => {
|
|
395
|
-
const node = buildNode(element, rootData, unit, {
|
|
396
|
-
...context,
|
|
397
|
-
contextData,
|
|
398
|
-
contextBinding,
|
|
399
|
-
});
|
|
400
|
-
if (node)
|
|
401
|
-
nodes.push(node);
|
|
402
|
-
});
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
if (el.binding && excluded.has(el.binding))
|
|
406
|
-
return;
|
|
407
278
|
const node = buildNode(el, rootData, unit, context);
|
|
408
279
|
if (node)
|
|
409
280
|
nodes.push(node);
|
|
@@ -441,6 +312,7 @@ const buildNode = (el, rootData, unit, context) => {
|
|
|
441
312
|
contentStyle: textStyle,
|
|
442
313
|
text: `${value}`,
|
|
443
314
|
visible: !!el.visible,
|
|
315
|
+
icon: buildTextIcon(el, unit),
|
|
444
316
|
};
|
|
445
317
|
}
|
|
446
318
|
if (el.type === 'image') {
|
|
@@ -477,15 +349,7 @@ const buildNode = (el, rootData, unit, context) => {
|
|
|
477
349
|
visible: !!el.visible,
|
|
478
350
|
};
|
|
479
351
|
}
|
|
480
|
-
|
|
481
|
-
return {
|
|
482
|
-
id: el.id,
|
|
483
|
-
type: 'text',
|
|
484
|
-
wrapperStyle,
|
|
485
|
-
contentStyle: baseStyle,
|
|
486
|
-
text: el.defaultValue || '',
|
|
487
|
-
visible: !!el.visible,
|
|
488
|
-
};
|
|
352
|
+
return null;
|
|
489
353
|
};
|
|
490
354
|
/**
|
|
491
355
|
* 主入口:合并布局 Schema 与数据,生成供前端使用的渲染结果。
|
|
@@ -521,4 +385,3 @@ const buildRenderResult = (layoutInput, dataInput, unit = 'px') => {
|
|
|
521
385
|
});
|
|
522
386
|
};
|
|
523
387
|
exports.buildRenderResult = buildRenderResult;
|
|
524
|
-
__exportStar(require("./utils"), exports);
|
package/dist/utils.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.backgroundChange = backgroundChange;
|
|
4
|
+
exports.stripLayoutBindings = stripLayoutBindings;
|
|
5
|
+
exports.applyItemCollectBindings = applyItemCollectBindings;
|
|
4
6
|
function backgroundChange(bg, layout) {
|
|
5
7
|
const toNameArray = (name) => {
|
|
6
8
|
if (Array.isArray(name))
|
|
@@ -43,16 +45,6 @@ function backgroundChange(bg, layout) {
|
|
|
43
45
|
children: traverse(el.children || []),
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
|
-
if (el.type === 'repeatable-group') {
|
|
47
|
-
return {
|
|
48
|
-
...el,
|
|
49
|
-
itemTemplate: traverse(el.itemTemplate || []),
|
|
50
|
-
items: (el.items || []).map(item => ({
|
|
51
|
-
...item,
|
|
52
|
-
elements: traverse(item.elements || []),
|
|
53
|
-
})),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
48
|
return applySpecialColor(el);
|
|
57
49
|
});
|
|
58
50
|
return {
|
|
@@ -62,3 +54,68 @@ function backgroundChange(bg, layout) {
|
|
|
62
54
|
children: traverse(layout.children || []),
|
|
63
55
|
};
|
|
64
56
|
}
|
|
57
|
+
function stripLayoutBindings(layouts = []) {
|
|
58
|
+
const targetLayouts = Array.isArray(layouts) ? layouts : [];
|
|
59
|
+
const stripElement = (el) => {
|
|
60
|
+
const { binding: _b, defaultValue: _d, ...rest } = el;
|
|
61
|
+
if (el.type === 'layout-panel') {
|
|
62
|
+
return {
|
|
63
|
+
...rest,
|
|
64
|
+
children: (el.children || []).map(stripElement),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return rest;
|
|
68
|
+
};
|
|
69
|
+
return targetLayouts.map(layout => ({
|
|
70
|
+
...layout,
|
|
71
|
+
children: (layout.children || []).map(stripElement),
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 应用元素数据绑定字段
|
|
76
|
+
* @param layouts 布局
|
|
77
|
+
* @param items 绑定元素数据
|
|
78
|
+
* @returns
|
|
79
|
+
*/
|
|
80
|
+
function applyItemCollectBindings(layouts = [], items = []) {
|
|
81
|
+
const targetLayouts = Array.isArray(layouts) ? layouts : [];
|
|
82
|
+
const metaMap = new Map();
|
|
83
|
+
const metaList = Array.isArray(items) ? items : [];
|
|
84
|
+
metaList.forEach(item => {
|
|
85
|
+
if (item && item.id !== undefined && item.id !== null) {
|
|
86
|
+
metaMap.set(String(item.id), item);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const assignBinding = (el) => {
|
|
90
|
+
const meta = metaMap.get(String(el.id));
|
|
91
|
+
const binding = meta && meta.bind !== undefined && meta.bind !== null
|
|
92
|
+
? meta.bind
|
|
93
|
+
: el.binding;
|
|
94
|
+
const defaultValue = meta && meta.default !== undefined ? meta.default : el.defaultValue;
|
|
95
|
+
const key = meta && meta.key !== undefined ? meta.key : el.key;
|
|
96
|
+
const base = { ...el };
|
|
97
|
+
if (binding !== undefined)
|
|
98
|
+
base.binding = binding;
|
|
99
|
+
else
|
|
100
|
+
delete base.binding;
|
|
101
|
+
if (defaultValue !== undefined)
|
|
102
|
+
base.defaultValue = defaultValue;
|
|
103
|
+
else
|
|
104
|
+
delete base.defaultValue;
|
|
105
|
+
if (key !== undefined)
|
|
106
|
+
base.key = key;
|
|
107
|
+
else
|
|
108
|
+
delete base.key;
|
|
109
|
+
if (el.type === 'layout-panel') {
|
|
110
|
+
return {
|
|
111
|
+
...base,
|
|
112
|
+
children: (el.children || []).map(assignBinding),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return base;
|
|
116
|
+
};
|
|
117
|
+
return targetLayouts.map(layout => ({
|
|
118
|
+
...layout,
|
|
119
|
+
children: (layout.children || []).map(assignBinding),
|
|
120
|
+
}));
|
|
121
|
+
}
|
package/index.ts
CHANGED
|
@@ -9,11 +9,10 @@ import type {
|
|
|
9
9
|
AbsoluteLayoutDefinition,
|
|
10
10
|
CardElement,
|
|
11
11
|
CardElementType,
|
|
12
|
-
FlexItemLayoutDefinition,
|
|
13
12
|
IconElement,
|
|
14
13
|
ImageElement,
|
|
15
14
|
LayoutPanelElement,
|
|
16
|
-
|
|
15
|
+
TextElement,
|
|
17
16
|
} from './interface/elements';
|
|
18
17
|
import type { CardLayoutInput, CardLayoutSchema } from './interface/layout';
|
|
19
18
|
import type {
|
|
@@ -29,7 +28,6 @@ export * from './interface';
|
|
|
29
28
|
|
|
30
29
|
const DEFAULT_CARD_WIDTH = 343; // 默认卡片宽度(像素)
|
|
31
30
|
const DEFAULT_CARD_HEIGHT = 210; // 默认卡片高度(像素)
|
|
32
|
-
const DEFAULT_GROUP_GAP = 22; // repeatable-item 默认纵向间距
|
|
33
31
|
|
|
34
32
|
const DIMENSION_PROPS = new Set<string>([
|
|
35
33
|
'width',
|
|
@@ -56,7 +54,6 @@ const DIMENSION_PROPS = new Set<string>([
|
|
|
56
54
|
'gap',
|
|
57
55
|
'rowGap',
|
|
58
56
|
'columnGap',
|
|
59
|
-
'flexBasis',
|
|
60
57
|
]);
|
|
61
58
|
|
|
62
59
|
/** ---------- 基础工具 ---------- */
|
|
@@ -136,11 +133,6 @@ const getAbsLayout = (el: CardElement): AbsoluteLayoutDefinition | null =>
|
|
|
136
133
|
? (el.layout as AbsoluteLayoutDefinition)
|
|
137
134
|
: null;
|
|
138
135
|
|
|
139
|
-
const getFlexLayout = (el: CardElement): FlexItemLayoutDefinition | null =>
|
|
140
|
-
el.layout && el.layout.mode === 'flex'
|
|
141
|
-
? (el.layout as FlexItemLayoutDefinition)
|
|
142
|
-
: null;
|
|
143
|
-
|
|
144
136
|
/** ---------- 布局与数据解析 ---------- */
|
|
145
137
|
|
|
146
138
|
/**
|
|
@@ -200,9 +192,7 @@ const readByPath = (data: any, path: string): any => {
|
|
|
200
192
|
return cursor;
|
|
201
193
|
};
|
|
202
194
|
/**
|
|
203
|
-
*
|
|
204
|
-
* - 全局 binding:直接基于根数据;
|
|
205
|
-
* - repeatable-group:binding 等于/前缀 dataPath 或以 `$item.` 开头时,改用当前条目数据。
|
|
195
|
+
* Resolve element binding against provided data.
|
|
206
196
|
*/
|
|
207
197
|
export const resolveBindingValue = (
|
|
208
198
|
binding: string | undefined,
|
|
@@ -210,26 +200,7 @@ export const resolveBindingValue = (
|
|
|
210
200
|
context?: BindingContext
|
|
211
201
|
): any => {
|
|
212
202
|
if (!binding) return undefined;
|
|
213
|
-
const
|
|
214
|
-
let target: any = rootData;
|
|
215
|
-
let path = binding;
|
|
216
|
-
|
|
217
|
-
// repeatable-group: binding 等于 dataPath 或以其为前缀时,切换到当前条目数据
|
|
218
|
-
if (
|
|
219
|
-
contextBinding &&
|
|
220
|
-
(binding === contextBinding || binding.startsWith(`${contextBinding}.`))
|
|
221
|
-
) {
|
|
222
|
-
target = contextData;
|
|
223
|
-
path =
|
|
224
|
-
binding === contextBinding
|
|
225
|
-
? ''
|
|
226
|
-
: binding.slice(contextBinding.length + 1);
|
|
227
|
-
} else if (binding.startsWith('$item.')) {
|
|
228
|
-
target = contextData;
|
|
229
|
-
path = binding.slice('$item.'.length);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const value = readByPath(target, path);
|
|
203
|
+
const value = readByPath(rootData, binding);
|
|
233
204
|
return value === undefined ? undefined : value;
|
|
234
205
|
};
|
|
235
206
|
|
|
@@ -257,68 +228,21 @@ const buildWrapperStyle = (el: CardElement, unit: 'px' | 'rpx'): string => {
|
|
|
257
228
|
);
|
|
258
229
|
}
|
|
259
230
|
|
|
260
|
-
|
|
261
|
-
if (!flex) return '';
|
|
262
|
-
const item:any = flex.item || {};
|
|
263
|
-
return styleObjectToString(
|
|
264
|
-
{
|
|
265
|
-
width: addUnit(flex.width, unit),
|
|
266
|
-
height: addUnit(flex.height, unit),
|
|
267
|
-
order: item.order,
|
|
268
|
-
flexGrow: item.flexGrow,
|
|
269
|
-
flexShrink: item.flexShrink,
|
|
270
|
-
flexBasis: item.flexBasis,
|
|
271
|
-
alignSelf: item.alignSelf,
|
|
272
|
-
boxSizing: 'border-box',
|
|
273
|
-
textAlign: el?.style?.textAlign as string | undefined,
|
|
274
|
-
},
|
|
275
|
-
unit
|
|
276
|
-
);
|
|
231
|
+
return '';
|
|
277
232
|
};
|
|
278
233
|
/**
|
|
279
234
|
* padding 数组/数字转 CSS 缩写字符串。
|
|
280
235
|
*/
|
|
281
|
-
const formatPadding = (
|
|
282
|
-
padding: number | number[] | undefined,
|
|
283
|
-
unit: 'px' | 'rpx'
|
|
284
|
-
) => {
|
|
285
|
-
if (padding === undefined || padding === null) return undefined;
|
|
286
|
-
if (typeof padding === 'number') return addUnit(padding, unit);
|
|
287
|
-
if (Array.isArray(padding)) {
|
|
288
|
-
const list = padding
|
|
289
|
-
.filter(v => v !== undefined && v !== null)
|
|
290
|
-
.map(v => addUnit(v as number, unit));
|
|
291
|
-
if (!list.length) return undefined;
|
|
292
|
-
if (list.length === 2) return `${list[0]} ${list[1]}`;
|
|
293
|
-
if (list.length === 3) return `${list[0]} ${list[1]} ${list[2]}`;
|
|
294
|
-
if (list.length >= 4) return `${list[0]} ${list[1]} ${list[2]} ${list[3]}`;
|
|
295
|
-
}
|
|
296
|
-
return undefined;
|
|
297
|
-
};
|
|
298
236
|
/**
|
|
299
|
-
* 构建 layout-panel
|
|
237
|
+
* 构建 layout-panel 的容器样式(绝对布局容器)。
|
|
300
238
|
*/
|
|
301
239
|
const buildPanelStyle = (
|
|
302
240
|
el: LayoutPanelElement,
|
|
303
241
|
unit: 'px' | 'rpx'
|
|
304
242
|
): string => {
|
|
305
|
-
const options = (el.container && el.container.options) || {};
|
|
306
243
|
const style = {
|
|
307
|
-
display: '
|
|
308
|
-
flexDirection: options.direction || 'row',
|
|
309
|
-
flexWrap: options.wrap || 'nowrap',
|
|
310
|
-
justifyContent: options.justifyContent,
|
|
311
|
-
alignItems: options.alignItems,
|
|
312
|
-
padding: formatPadding(options.padding as any, unit),
|
|
244
|
+
display: 'block',
|
|
313
245
|
} as Record<string, any>;
|
|
314
|
-
|
|
315
|
-
if (options.gap && typeof options.gap === 'number') {
|
|
316
|
-
style.gap = addUnit(options.gap, unit);
|
|
317
|
-
} else if (options.gap && isObject(options.gap)) {
|
|
318
|
-
style.rowGap = addUnit((options.gap as any).row, unit);
|
|
319
|
-
style.columnGap = addUnit((options.gap as any).column, unit);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
246
|
return styleObjectToString(style, unit);
|
|
323
247
|
};
|
|
324
248
|
/**
|
|
@@ -332,91 +256,46 @@ const normalizeElementStyle = (
|
|
|
332
256
|
return styleObjectToString(style, unit);
|
|
333
257
|
};
|
|
334
258
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
rootData: Record<string, any>
|
|
362
|
-
): { element: CardElement; contextData: any; contextBinding: string }[] => {
|
|
363
|
-
const result: {
|
|
364
|
-
element: CardElement;
|
|
365
|
-
contextData: any;
|
|
366
|
-
contextBinding: string;
|
|
367
|
-
}[] = [];
|
|
368
|
-
const dataset = resolveBindingValue(group.dataPath, rootData, {}) || [];
|
|
369
|
-
const dataList = Array.isArray(dataset) ? dataset : [];
|
|
370
|
-
const maxItems =
|
|
371
|
-
group.maxItems ||
|
|
372
|
-
dataList.length ||
|
|
373
|
-
(group.items || []).length ||
|
|
374
|
-
(group.itemTemplate || []).length;
|
|
375
|
-
const template =
|
|
376
|
-
(group.items && group.items[0] && group.items[0].elements) ||
|
|
377
|
-
group.itemTemplate ||
|
|
378
|
-
[];
|
|
379
|
-
const gap = inferGroupGap(group);
|
|
380
|
-
|
|
381
|
-
// Use saved items (from designer) first
|
|
382
|
-
if (group.items && group.items.length) {
|
|
383
|
-
group.items.slice(0, maxItems).forEach((item, idx) => {
|
|
384
|
-
const payload = dataList[idx] !== undefined ? dataList[idx] : item.data;
|
|
385
|
-
(item.elements || []).forEach(el => {
|
|
386
|
-
result.push({
|
|
387
|
-
element: el,
|
|
388
|
-
contextData: payload,
|
|
389
|
-
contextBinding: group.dataPath,
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
return result;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Otherwise clone from template by gap
|
|
397
|
-
dataList.slice(0, maxItems).forEach((payload, idx) => {
|
|
398
|
-
template.forEach(el => {
|
|
399
|
-
const abs = getAbsLayout(el);
|
|
400
|
-
const cloned = abs
|
|
401
|
-
? ({
|
|
402
|
-
...el,
|
|
403
|
-
layout: { ...abs, y: abs.y + idx * gap },
|
|
404
|
-
} as CardElement)
|
|
405
|
-
: el;
|
|
406
|
-
result.push({
|
|
407
|
-
element: cloned,
|
|
408
|
-
contextData: payload,
|
|
409
|
-
contextBinding: group.dataPath,
|
|
410
|
-
});
|
|
411
|
-
});
|
|
412
|
-
});
|
|
259
|
+
const buildTextIcon = (
|
|
260
|
+
el: TextElement,
|
|
261
|
+
unit: 'px' | 'rpx'
|
|
262
|
+
): RenderNode['icon'] | undefined => {
|
|
263
|
+
const icon = el.icon;
|
|
264
|
+
if (!icon || icon.enable === false) return undefined;
|
|
265
|
+
|
|
266
|
+
const style = icon.style || 'fill';
|
|
267
|
+
const baseName = el.key || el.binding || el.id;
|
|
268
|
+
let name: string | undefined;
|
|
269
|
+
if (style === 'dot') name = 'round';
|
|
270
|
+
else if (style === 'line') name = baseName ? `${baseName}-line` : undefined;
|
|
271
|
+
else name = baseName || undefined;
|
|
272
|
+
if (!name) return undefined;
|
|
273
|
+
|
|
274
|
+
const size =
|
|
275
|
+
icon.size !== undefined && icon.size !== null
|
|
276
|
+
? icon.size
|
|
277
|
+
: (el.style?.fontSize as any);
|
|
278
|
+
const gap =
|
|
279
|
+
icon.gap !== undefined && icon.gap !== null ? icon.gap : 4;
|
|
280
|
+
const color =
|
|
281
|
+
icon.color ??
|
|
282
|
+
((typeof el.style?.color === 'string' ? el.style.color : undefined) as
|
|
283
|
+
| string
|
|
284
|
+
| undefined);
|
|
413
285
|
|
|
414
|
-
return
|
|
286
|
+
return {
|
|
287
|
+
name: `${name}`,
|
|
288
|
+
text: `${name}`,
|
|
289
|
+
size: addUnit(size as any, unit),
|
|
290
|
+
gap: addUnit(gap as any, unit),
|
|
291
|
+
color: color as any,
|
|
292
|
+
align: icon.align || 'left',
|
|
293
|
+
};
|
|
415
294
|
};
|
|
416
295
|
|
|
417
296
|
/** ---------- 渲染树构建 ---------- */
|
|
418
297
|
/**
|
|
419
|
-
* 将 children
|
|
298
|
+
* 将 children 展开为渲染节点。
|
|
420
299
|
*/
|
|
421
300
|
export const buildRenderNodes = (
|
|
422
301
|
children: CardElement[],
|
|
@@ -426,34 +305,9 @@ export const buildRenderNodes = (
|
|
|
426
305
|
): RenderNode[] => {
|
|
427
306
|
if (!Array.isArray(children)) return [];
|
|
428
307
|
|
|
429
|
-
// Mark mutually exclusive bindings to hide them when repeatable-group is present
|
|
430
|
-
const excluded = new Set<string>();
|
|
431
|
-
children.forEach(el => {
|
|
432
|
-
if (el && el.type === 'repeatable-group' && !!el.visible) {
|
|
433
|
-
(el.mutualExcludes || []).forEach(b => excluded.add(b));
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
|
|
437
308
|
const nodes: RenderNode[] = [];
|
|
438
309
|
children.forEach(el => {
|
|
439
310
|
if (!el || el.visible === false) return;
|
|
440
|
-
if (el.type === 'repeatable-group') {
|
|
441
|
-
const instances = materializeRepeatableItems(
|
|
442
|
-
el as RepeatableGroupElement,
|
|
443
|
-
rootData
|
|
444
|
-
);
|
|
445
|
-
instances.forEach(({ element, contextData, contextBinding }) => {
|
|
446
|
-
const node = buildNode(element, rootData, unit, {
|
|
447
|
-
...context,
|
|
448
|
-
contextData,
|
|
449
|
-
contextBinding,
|
|
450
|
-
});
|
|
451
|
-
if (node) nodes.push(node);
|
|
452
|
-
});
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (el.binding && excluded.has(el.binding)) return;
|
|
457
311
|
const node = buildNode(el, rootData, unit, context);
|
|
458
312
|
if (node) nodes.push(node);
|
|
459
313
|
});
|
|
@@ -498,6 +352,7 @@ const buildNode = (
|
|
|
498
352
|
contentStyle: textStyle,
|
|
499
353
|
text: `${value}`,
|
|
500
354
|
visible: !!el.visible,
|
|
355
|
+
icon: buildTextIcon(el as TextElement, unit),
|
|
501
356
|
};
|
|
502
357
|
}
|
|
503
358
|
|
|
@@ -537,26 +392,18 @@ const buildNode = (
|
|
|
537
392
|
};
|
|
538
393
|
}
|
|
539
394
|
|
|
540
|
-
if (el.type === 'custom') {
|
|
541
|
-
return {
|
|
542
|
-
id: el.id,
|
|
543
|
-
type: el.type,
|
|
544
|
-
wrapperStyle,
|
|
545
|
-
contentStyle: baseStyle,
|
|
546
|
-
visible: !!el.visible,
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
id: el.id,
|
|
553
|
-
type: 'text',
|
|
554
|
-
wrapperStyle,
|
|
555
|
-
contentStyle: baseStyle,
|
|
556
|
-
text: el.defaultValue || '',
|
|
557
|
-
visible: !!el.visible,
|
|
558
|
-
};
|
|
559
|
-
};
|
|
395
|
+
if (el.type === 'custom') {
|
|
396
|
+
return {
|
|
397
|
+
id: el.id,
|
|
398
|
+
type: el.type,
|
|
399
|
+
wrapperStyle,
|
|
400
|
+
contentStyle: baseStyle,
|
|
401
|
+
visible: !!el.visible,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return null;
|
|
406
|
+
};
|
|
560
407
|
/**
|
|
561
408
|
* 主入口:合并布局 Schema 与数据,生成供前端使用的渲染结果。
|
|
562
409
|
*/
|
|
@@ -609,6 +456,3 @@ export const buildRenderResult = (
|
|
|
609
456
|
};
|
|
610
457
|
});
|
|
611
458
|
};
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
export * from './utils'
|
package/interface/elements.ts
CHANGED
|
@@ -3,8 +3,7 @@ export type CardElementType =
|
|
|
3
3
|
| 'image'
|
|
4
4
|
| 'icon'
|
|
5
5
|
| 'custom'
|
|
6
|
-
| 'layout-panel'
|
|
7
|
-
| 'repeatable-group';
|
|
6
|
+
| 'layout-panel';
|
|
8
7
|
|
|
9
8
|
export interface AbsoluteLayoutDefinition {
|
|
10
9
|
mode: 'absolute';
|
|
@@ -15,37 +14,31 @@ export interface AbsoluteLayoutDefinition {
|
|
|
15
14
|
zIndex?: number;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
export interface FlexItemOptions {
|
|
19
|
-
flexGrow?: number;
|
|
20
|
-
flexShrink?: number;
|
|
21
|
-
flexBasis?: number | string;
|
|
22
|
-
order?: number;
|
|
23
|
-
alignSelf?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface FlexItemLayoutDefinition {
|
|
27
|
-
mode: 'flex';
|
|
28
|
-
item?: FlexItemOptions;
|
|
29
|
-
width?: number;
|
|
30
|
-
height?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type ElementLayout = AbsoluteLayoutDefinition | FlexItemLayoutDefinition;
|
|
34
|
-
|
|
35
17
|
export interface CardElementBase {
|
|
36
18
|
id: string;
|
|
37
19
|
type: CardElementType;
|
|
38
|
-
layout:
|
|
20
|
+
layout: AbsoluteLayoutDefinition;
|
|
39
21
|
visible?: boolean;
|
|
40
22
|
binding?: string;
|
|
41
23
|
style?: Record<string, string | number | undefined>;
|
|
42
24
|
defaultValue?: string;
|
|
25
|
+
key?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TextIconConfig {
|
|
29
|
+
name?: string;
|
|
30
|
+
size?: number;
|
|
31
|
+
color?: string;
|
|
32
|
+
gap?: number;
|
|
33
|
+
align?: 'left' | 'right';
|
|
34
|
+
enable?: boolean;
|
|
35
|
+
style?: 'fill' | 'dot' | 'line';
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
export interface TextElement extends CardElementBase {
|
|
46
39
|
type: 'text';
|
|
47
40
|
align?: 'left' | 'center' | 'right';
|
|
48
|
-
|
|
41
|
+
icon?: TextIconConfig;
|
|
49
42
|
}
|
|
50
43
|
|
|
51
44
|
export interface ImageElement extends CardElementBase {
|
|
@@ -67,44 +60,14 @@ export interface CustomElement extends CardElementBase {
|
|
|
67
60
|
export interface LayoutPanelElement extends CardElementBase {
|
|
68
61
|
type: 'layout-panel';
|
|
69
62
|
container: {
|
|
70
|
-
mode: 'absolute'
|
|
71
|
-
options?: {
|
|
72
|
-
direction?: 'row' | 'column';
|
|
73
|
-
wrap?: 'nowrap' | 'wrap';
|
|
74
|
-
justifyContent?:
|
|
75
|
-
| 'flex-start'
|
|
76
|
-
| 'flex-end'
|
|
77
|
-
| 'center'
|
|
78
|
-
| 'space-between'
|
|
79
|
-
| 'space-around'
|
|
80
|
-
| 'space-evenly';
|
|
81
|
-
alignItems?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
|
|
82
|
-
gap?: number | { row: number; column: number };
|
|
83
|
-
padding?: number | [number, number] | [number, number, number, number];
|
|
84
|
-
};
|
|
63
|
+
mode: 'absolute';
|
|
85
64
|
};
|
|
86
65
|
children?: CardElement[];
|
|
87
66
|
}
|
|
88
67
|
|
|
89
|
-
export interface RepeatableGroupItem {
|
|
90
|
-
id: string;
|
|
91
|
-
elements: CardElement[];
|
|
92
|
-
data?: Record<string, any>;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export interface RepeatableGroupElement extends CardElementBase {
|
|
96
|
-
type: 'repeatable-group';
|
|
97
|
-
dataPath: string;
|
|
98
|
-
itemTemplate: CardElement[];
|
|
99
|
-
items: RepeatableGroupItem[];
|
|
100
|
-
maxItems?: number;
|
|
101
|
-
mutualExcludes?: string[];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
68
|
export type CardElement =
|
|
105
69
|
| TextElement
|
|
106
70
|
| ImageElement
|
|
107
71
|
| IconElement
|
|
108
72
|
| CustomElement
|
|
109
|
-
| LayoutPanelElement
|
|
110
|
-
| RepeatableGroupElement;
|
|
73
|
+
| LayoutPanelElement;
|
package/interface/render.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { CardElementType, CardElement } from './elements';
|
|
2
2
|
import type { CardLayoutInput, CardLayoutSchema } from './layout';
|
|
3
3
|
|
|
4
|
+
export interface RenderNodeIcon {
|
|
5
|
+
name: string;
|
|
6
|
+
text?: string;
|
|
7
|
+
size?: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
gap?: string;
|
|
10
|
+
align?: 'left' | 'right';
|
|
11
|
+
}
|
|
12
|
+
|
|
4
13
|
export interface RenderNode {
|
|
5
14
|
id: string;
|
|
6
15
|
type: CardElementType | 'text';
|
|
@@ -12,6 +21,7 @@ export interface RenderNode {
|
|
|
12
21
|
mode?: string;
|
|
13
22
|
children?: RenderNode[];
|
|
14
23
|
visible?: boolean;
|
|
24
|
+
icon?: RenderNodeIcon;
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
export interface RenderPageResult {
|
|
@@ -25,11 +35,11 @@ export type RenderResult = RenderPageResult[];
|
|
|
25
35
|
|
|
26
36
|
export interface BindingContext {
|
|
27
37
|
/**
|
|
28
|
-
*
|
|
38
|
+
* 当前上下文数据的绑定前缀(预留给嵌套场景)
|
|
29
39
|
*/
|
|
30
40
|
contextBinding?: string;
|
|
31
41
|
/**
|
|
32
|
-
*
|
|
42
|
+
* 当前上下文数据(预留给嵌套场景)
|
|
33
43
|
*/
|
|
34
44
|
contextData?: any;
|
|
35
45
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "km-card-layout-core",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Shared render helpers for CardMaster layout JSON (binding resolution,
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Shared render helpers for CardMaster layout JSON (binding resolution, layout normalization).",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "types.d.ts",
|
|
7
7
|
"license": "MIT",
|
package/types.d.ts
CHANGED
|
@@ -14,6 +14,13 @@ import type {
|
|
|
14
14
|
RenderResult,
|
|
15
15
|
} from './interface';
|
|
16
16
|
|
|
17
|
+
export type ItemCollectMeta = {
|
|
18
|
+
id: string | number;
|
|
19
|
+
bind?: string | null;
|
|
20
|
+
default?: string | null;
|
|
21
|
+
key?: string | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
17
24
|
export function addUnit(
|
|
18
25
|
value: string | number | undefined | null,
|
|
19
26
|
unit: 'px' | 'rpx'
|
|
@@ -32,6 +39,15 @@ export function resolveBindingValue(
|
|
|
32
39
|
context?: BindingContext
|
|
33
40
|
): any;
|
|
34
41
|
|
|
42
|
+
export function stripLayoutBindings(
|
|
43
|
+
layouts?: CardLayoutSchema[]
|
|
44
|
+
): CardLayoutSchema[];
|
|
45
|
+
|
|
46
|
+
export function applyItemCollectBindings(
|
|
47
|
+
layouts: CardLayoutSchema[],
|
|
48
|
+
items?: ItemCollectMeta[]
|
|
49
|
+
): CardLayoutSchema[];
|
|
50
|
+
|
|
35
51
|
export function buildRenderNodes(
|
|
36
52
|
children: CardElement[],
|
|
37
53
|
data: Record<string, any>,
|
package/utils.ts
CHANGED
|
@@ -1,67 +1,136 @@
|
|
|
1
|
-
import type { CardElement, CardLayoutSchema } from './interface';
|
|
2
|
-
import { TemplateBackground } from './interface/data/payload';
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
);
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (el
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
1
|
+
import type { CardElement, CardLayoutSchema } from './interface';
|
|
2
|
+
import { TemplateBackground } from './interface/data/payload';
|
|
3
|
+
|
|
4
|
+
export type ItemCollectMeta = {
|
|
5
|
+
id: string | number;
|
|
6
|
+
bind?: string | null;
|
|
7
|
+
default?: string | null;
|
|
8
|
+
key?: string | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function backgroundChange(
|
|
12
|
+
bg: TemplateBackground,
|
|
13
|
+
layout: CardLayoutSchema
|
|
14
|
+
): CardLayoutSchema {
|
|
15
|
+
const toNameArray = (name?: string | string[]) => {
|
|
16
|
+
if (Array.isArray(name)) return name.filter(Boolean);
|
|
17
|
+
if (!name) return [];
|
|
18
|
+
return `${name}`
|
|
19
|
+
.split(',')
|
|
20
|
+
.map(n => n.trim())
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const applySpecialColor = (el: CardElement): CardElement => {
|
|
25
|
+
const extras = bg.fontColorExtra || [];
|
|
26
|
+
if (!extras.length) return el;
|
|
27
|
+
|
|
28
|
+
const keys: string[] = [];
|
|
29
|
+
if (el.binding) keys.push(String(el.binding));
|
|
30
|
+
if (el.id) keys.push(String(el.id));
|
|
31
|
+
if (el.type === 'icon') keys.push('icon');
|
|
32
|
+
if (el.type === 'custom') keys.push('decor');
|
|
33
|
+
|
|
34
|
+
const matched = extras.find(sc =>
|
|
35
|
+
toNameArray(sc.name).some(n => keys.some(k => k?.startsWith(n)))
|
|
36
|
+
);
|
|
37
|
+
if (!matched) return el;
|
|
38
|
+
|
|
39
|
+
const baseStyle = { ...(el.style || {}) };
|
|
40
|
+
if (el.type === 'custom') {
|
|
41
|
+
return { ...el, style: { ...baseStyle, backgroundColor: matched.color } };
|
|
42
|
+
}
|
|
43
|
+
return { ...el, style: { ...baseStyle, color: matched.color } };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const traverse = (children: CardElement[] = []): CardElement[] =>
|
|
47
|
+
children.map(el => {
|
|
48
|
+
if (!el) return el;
|
|
49
|
+
if (el.type === 'layout-panel') {
|
|
50
|
+
return {
|
|
51
|
+
...el,
|
|
52
|
+
children: traverse(el.children || []),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return applySpecialColor(el);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
...layout,
|
|
60
|
+
backgroundImage: bg.imgUrl,
|
|
61
|
+
fontColor: bg.fontColor || layout.fontColor,
|
|
62
|
+
children: traverse(layout.children || []),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function stripLayoutBindings(
|
|
67
|
+
layouts: CardLayoutSchema[] = []
|
|
68
|
+
): CardLayoutSchema[] {
|
|
69
|
+
const targetLayouts = Array.isArray(layouts) ? layouts : [];
|
|
70
|
+
const stripElement = (el: CardElement): CardElement => {
|
|
71
|
+
const { binding: _b, defaultValue: _d, ...rest } = el as any;
|
|
72
|
+
if (el.type === 'layout-panel') {
|
|
73
|
+
return {
|
|
74
|
+
...rest,
|
|
75
|
+
children: (el.children || []).map(stripElement),
|
|
76
|
+
} as CardElement;
|
|
77
|
+
}
|
|
78
|
+
return rest as CardElement;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return targetLayouts.map(layout => ({
|
|
82
|
+
...layout,
|
|
83
|
+
children: (layout.children || []).map(stripElement),
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 应用元素数据绑定字段
|
|
89
|
+
* @param layouts 布局
|
|
90
|
+
* @param items 绑定元素数据
|
|
91
|
+
* @returns
|
|
92
|
+
*/
|
|
93
|
+
export function applyItemCollectBindings(
|
|
94
|
+
layouts: CardLayoutSchema[] = [],
|
|
95
|
+
items: ItemCollectMeta[] = []
|
|
96
|
+
): CardLayoutSchema[] {
|
|
97
|
+
const targetLayouts = Array.isArray(layouts) ? layouts : [];
|
|
98
|
+
const metaMap = new Map<string, ItemCollectMeta>();
|
|
99
|
+
const metaList = Array.isArray(items) ? items : [];
|
|
100
|
+
metaList.forEach(item => {
|
|
101
|
+
if (item && item.id !== undefined && item.id !== null) {
|
|
102
|
+
metaMap.set(String(item.id), item);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const assignBinding = (el: CardElement): CardElement => {
|
|
107
|
+
const meta = metaMap.get(String(el.id));
|
|
108
|
+
const binding =
|
|
109
|
+
meta && meta.bind !== undefined && meta.bind !== null
|
|
110
|
+
? meta.bind
|
|
111
|
+
: el.binding;
|
|
112
|
+
const defaultValue =
|
|
113
|
+
meta && meta.default !== undefined ? meta.default : el.defaultValue;
|
|
114
|
+
const key = meta && meta.key !== undefined ? meta.key : el.key;
|
|
115
|
+
const base: any = { ...el };
|
|
116
|
+
if (binding !== undefined) base.binding = binding;
|
|
117
|
+
else delete base.binding;
|
|
118
|
+
if (defaultValue !== undefined) base.defaultValue = defaultValue;
|
|
119
|
+
else delete base.defaultValue;
|
|
120
|
+
if (key !== undefined) base.key = key;
|
|
121
|
+
else delete base.key;
|
|
122
|
+
|
|
123
|
+
if (el.type === 'layout-panel') {
|
|
124
|
+
return {
|
|
125
|
+
...base,
|
|
126
|
+
children: (el.children || []).map(assignBinding),
|
|
127
|
+
} as CardElement;
|
|
128
|
+
}
|
|
129
|
+
return base as CardElement;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return targetLayouts.map(layout => ({
|
|
133
|
+
...layout,
|
|
134
|
+
children: (layout.children || []).map(assignBinding),
|
|
135
|
+
}));
|
|
136
|
+
}
|