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/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
- RepeatableGroupElement,
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
- * 解析元素 binding 对应的数据:
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 { contextBinding, contextData } = context || {};
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
- const flex = getFlexLayout(el);
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 的容器样式(flex)。
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: 'flex',
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
- /** ---------- Repeatable 相关 ---------- */
336
- /**
337
- * 通过前两条 item 的布局推断间距,不足则回落 DEFAULT_GROUP_GAP。
338
- */
339
- const inferGroupGap = (group: RepeatableGroupElement): number => {
340
- const items = group.items || [];
341
- if (items.length >= 2) {
342
- const first = (items[0].elements || []).map(getAbsLayout).find(Boolean);
343
- const second = (items[1].elements || []).map(getAbsLayout).find(Boolean);
344
- if (
345
- first &&
346
- second &&
347
- typeof first.y === 'number' &&
348
- typeof second.y === 'number'
349
- ) {
350
- return Math.abs(second.y - first.y);
351
- }
352
- }
353
- return DEFAULT_GROUP_GAP;
354
- };
355
- /**
356
- * 展开 repeatable-group 为具体元素实例,并附带条目数据。
357
- * 优先使用设计器保存的 `items`;否则按推断间距克隆 `itemTemplate`。
358
- */
359
- const materializeRepeatableItems = (
360
- group: RepeatableGroupElement,
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 result;
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 展开为渲染节点,处理 repeatable-group 展开与 mutualExcludes 隐藏。
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
- // Unknown type fallback to simple text node
551
- return {
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'
@@ -18,7 +18,10 @@ export interface TemplateItem {
18
18
  id: number;
19
19
  name: string;
20
20
  bind: string;
21
+ key?: string;
22
+ icon?: string;
21
23
  default?: string;
22
24
  type: 'text' | 'image';
23
25
  cate: number;
24
- }
26
+ required?: 0 | 1;
27
+ }
@@ -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: ElementLayout;
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
- multiline?: boolean;
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' | 'flex';
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;
@@ -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
- * 当前上下文数据的绑定前缀(repeatable-group 内为 dataPath)
38
+ * 当前上下文数据的绑定前缀(预留给嵌套场景)
29
39
  */
30
40
  contextBinding?: string;
31
41
  /**
32
- * 当前上下文数据(repeatable-group 单条数据)
42
+ * 当前上下文数据(预留给嵌套场景)
33
43
  */
34
44
  contextData?: any;
35
45
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "km-card-layout-core",
3
- "version": "0.1.0",
4
- "description": "Shared render helpers for CardMaster layout JSON (binding resolution, repeatable groups, layout normalization).",
5
- "main": "index.js",
3
+ "version": "0.1.3",
4
+ "description": "Shared render helpers for CardMaster layout JSON (binding resolution, layout normalization).",
5
+ "main": "dist/index.js",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",
8
8
  "scripts": {
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>,