km-card-layout-core 0.1.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # km-card-layout-core
2
2
 
3
- 纯函数渲染核心:负责将布局 JSON 与数据合成渲染树,处理绑定路径、repeatable-group 展开、样式单位(px/rpx)等。
3
+ 纯函数渲染核心:负责将布局 JSON 与数据合成渲染树,处理绑定路径、样式单位(px/rpx)等。
4
4
 
5
5
  ## API
6
6
 
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
- * 解析元素 binding 对应的数据:
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 { contextBinding, contextData } = context || {};
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, _b;
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
- const flex = getFlexLayout(el);
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 的容器样式(flex)。
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: 'flex',
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
- /** ---------- Repeatable 相关 ---------- */
308
- /**
309
- * 通过前两条 item 的布局推断间距,不足则回落 DEFAULT_GROUP_GAP。
310
- */
311
- const inferGroupGap = (group) => {
312
- const items = group.items || [];
313
- if (items.length >= 2) {
314
- const first = (items[0].elements || []).map(getAbsLayout).find(Boolean);
315
- const second = (items[1].elements || []).map(getAbsLayout).find(Boolean);
316
- if (first &&
317
- second &&
318
- typeof first.y === 'number' &&
319
- typeof second.y === 'number') {
320
- return Math.abs(second.y - first.y);
321
- }
322
- }
323
- return DEFAULT_GROUP_GAP;
324
- };
325
- /**
326
- * 展开 repeatable-group 为具体元素实例,并附带条目数据。
327
- * 优先使用设计器保存的 `items`;否则按推断间距克隆 `itemTemplate`。
328
- */
329
- const materializeRepeatableItems = (group, rootData) => {
330
- const result = [];
331
- const dataset = (0, exports.resolveBindingValue)(group.dataPath, rootData, {}) || [];
332
- const dataList = Array.isArray(dataset) ? dataset : [];
333
- const maxItems = group.maxItems ||
334
- dataList.length ||
335
- (group.items || []).length ||
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 展开为渲染节点,处理 repeatable-group 展开与 mutualExcludes 隐藏。
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
- // Unknown type fallback to simple text node
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
+ }