km-card-layout-component-miniprogram 0.1.5 → 0.1.6

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.
@@ -5,167 +5,29 @@
5
5
  * - 职责:将布局 Schema 与业务数据合成为带内联样式的渲染树,外层只需将节点映射到各端组件。
6
6
  */
7
7
 
8
- /** ---------- 类型定义 ---------- */
9
-
10
- export type CardElementType =
11
- | 'text'
12
- | 'image'
13
- | 'icon'
14
- | 'custom'
15
- | 'layout-panel'
16
- | 'repeatable-group';
17
-
18
- export interface AbsoluteLayoutDefinition {
19
- mode: 'absolute';
20
- x: number;
21
- y: number;
22
- width: number;
23
- height: number;
24
- zIndex?: number;
25
- }
26
-
27
- export interface FlexItemOptions {
28
- flexGrow?: number;
29
- flexShrink?: number;
30
- flexBasis?: number | string;
31
- order?: number;
32
- alignSelf?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
33
- }
34
-
35
- export interface FlexItemLayoutDefinition {
36
- mode: 'flex';
37
- item?: FlexItemOptions;
38
- width?: number;
39
- height?: number;
40
- }
41
-
42
- export type ElementLayout = AbsoluteLayoutDefinition | FlexItemLayoutDefinition;
43
-
44
- export interface CardElementBase {
45
- id: string;
46
- type: CardElementType;
47
- layout: ElementLayout;
48
- visible?: boolean;
49
- binding?: string;
50
- style?: Record<string, string | number | undefined>;
51
- defaultValue?: string;
52
- }
53
-
54
- export interface TextElement extends CardElementBase {
55
- type: 'text';
56
- align?: 'left' | 'center' | 'right';
57
- multiline?: boolean;
58
- }
59
-
60
- export interface ImageElement extends CardElementBase {
61
- type: 'image';
62
- alt?: string;
63
- fit?: 'cover' | 'contain';
64
- defaultUrl?: string;
65
- }
66
-
67
- export interface IconElement extends CardElementBase {
68
- type: 'icon';
69
- name?: string;
70
- }
71
-
72
- export interface CustomElement extends CardElementBase {
73
- type: 'custom';
74
- }
75
-
76
- export interface LayoutPanelElement extends CardElementBase {
77
- type: 'layout-panel';
78
- container: {
79
- mode: 'absolute' | 'flex';
80
- options?: {
81
- direction?: 'row' | 'column';
82
- wrap?: 'nowrap' | 'wrap';
83
- justifyContent?:
84
- | 'flex-start'
85
- | 'flex-end'
86
- | 'center'
87
- | 'space-between'
88
- | 'space-around'
89
- | 'space-evenly';
90
- alignItems?:
91
- | 'flex-start'
92
- | 'flex-end'
93
- | 'center'
94
- | 'stretch'
95
- | 'baseline';
96
- gap?: number | { row: number; column: number };
97
- padding?: number | [number, number] | [number, number, number, number];
98
- };
99
- };
100
- children?: CardElement[];
101
- }
102
-
103
- export interface RepeatableGroupItem {
104
- id: string;
105
- elements: CardElement[];
106
- data?: Record<string, any>;
107
- }
108
-
109
- export interface RepeatableGroupElement extends CardElementBase {
110
- type: 'repeatable-group';
111
- dataPath: string;
112
- itemTemplate: CardElement[];
113
- items: RepeatableGroupItem[];
114
- maxItems?: number;
115
- mutualExcludes?: string[];
116
- }
117
-
118
- export type CardElement =
119
- | TextElement
120
- | ImageElement
121
- | IconElement
122
- | CustomElement
123
- | LayoutPanelElement
124
- | RepeatableGroupElement;
125
-
126
- export interface CardLayoutSchema {
127
- name?: string;
128
- container: {
129
- mode: 'absolute';
130
- };
131
- width: number;
132
- height: number;
133
- backgroundImage?: string;
134
- backgroundZIndex?: number;
135
- fontColor?: string;
136
- borderRadius?: number;
137
- padding?: number;
138
- children: CardElement[];
139
- thumbnail?: string;
140
- }
141
-
142
- export interface RenderNode {
143
- id: string;
144
- type: CardElementType | 'text';
145
- wrapperStyle: string;
146
- contentStyle: string;
147
- name?: string;
148
- text?: string;
149
- src?: string;
150
- mode?: string;
151
- children?: RenderNode[];
152
- visible?: boolean;
153
- }
154
-
155
- export interface RenderPageResult {
156
- renderTree: RenderNode[];
157
- cardStyle: string;
158
- backgroundImage: string;
159
- backgroundStyle: string;
160
- }
161
-
162
- export type RenderResult = RenderPageResult[];
8
+ import type {
9
+ AbsoluteLayoutDefinition,
10
+ CardElement,
11
+ CardElementType,
12
+ IconElement,
13
+ ImageElement,
14
+ LayoutPanelElement,
15
+ TextElement,
16
+ } from './interface/elements';
17
+ import type { CardLayoutInput, CardLayoutSchema } from './interface/layout';
18
+ import type {
19
+ BindingContext,
20
+ RenderNode,
21
+ RenderPageResult,
22
+ RenderResult,
23
+ } from './interface/render';
24
+
25
+ export * from './interface/index';
163
26
 
164
27
  /** ---------- 常量 ---------- */
165
28
 
166
29
  const DEFAULT_CARD_WIDTH = 343; // 默认卡片宽度(像素)
167
30
  const DEFAULT_CARD_HEIGHT = 210; // 默认卡片高度(像素)
168
- const DEFAULT_GROUP_GAP = 22; // repeatable-item 默认纵向间距
169
31
 
170
32
  const DIMENSION_PROPS = new Set<string>([
171
33
  'width',
@@ -192,7 +54,6 @@ const DIMENSION_PROPS = new Set<string>([
192
54
  'gap',
193
55
  'rowGap',
194
56
  'columnGap',
195
- 'flexBasis',
196
57
  ]);
197
58
 
198
59
  /** ---------- 基础工具 ---------- */
@@ -212,44 +73,44 @@ const toKebab = (key: string): string =>
212
73
  * 数字追加单位,非数字原样转字符串。
213
74
  * 数字追加单位,非数字原样转字符串。
214
75
  */
215
- export const addUnit = (
216
- value: string | number | undefined | null,
217
- unit: 'px' | 'rpx'
218
- ): string | undefined => {
219
- if (value === undefined || value === null || value === '') return undefined;
220
- if (typeof value === 'number') {
221
- const ratio = unit === 'rpx' ? 2 : 1;
222
- return `${value * ratio}${unit}`;
223
- }
224
- if (typeof value === 'string') {
225
- const parsed = Number(value);
226
- if (Number.isFinite(parsed)) {
227
- const ratio = unit === 'rpx' ? 2 : 1;
228
- return `${parsed * ratio}${unit}`;
229
- }
230
- }
231
- return `${value}`;
232
- };
76
+ export const addUnit = (
77
+ value: string | number | undefined | null,
78
+ unit: 'px' | 'rpx'
79
+ ): string | undefined => {
80
+ if (value === undefined || value === null || value === '') return undefined;
81
+ if (typeof value === 'number') {
82
+ const ratio = unit === 'rpx' ? 2 : 1;
83
+ return `${value * ratio}${unit}`;
84
+ }
85
+ if (typeof value === 'string') {
86
+ const parsed = Number(value);
87
+ if (Number.isFinite(parsed)) {
88
+ const ratio = unit === 'rpx' ? 2 : 1;
89
+ return `${parsed * ratio}${unit}`;
90
+ }
91
+ }
92
+ return `${value}`;
93
+ };
233
94
  /**
234
95
  * 样式对象转内联样式字符串,对需要单位的字段自动补单位。
235
96
  */
236
- export const styleObjectToString = (
237
- style?: Record<string, any>,
238
- unit: 'px' | 'rpx' = 'px'
239
- ): string => {
240
- if (!style) return '';
241
- const pairs: string[] = [];
242
- Object.keys(style).forEach(key => {
243
- const value = style[key];
244
- if (value === undefined || value === null || value === '') return;
245
- const useUnit = DIMENSION_PROPS.has(key)
246
- ? addUnit(value as any, unit)
247
- : value;
248
- if (useUnit === undefined || useUnit === null || useUnit === '') return;
249
- pairs.push(`${toKebab(key)}:${useUnit}`);
250
- });
251
- return pairs.join(';');
252
- };
97
+ export const styleObjectToString = (
98
+ style?: Record<string, any>,
99
+ unit: 'px' | 'rpx' = 'px'
100
+ ): string => {
101
+ if (!style) return '';
102
+ const pairs: string[] = [];
103
+ Object.keys(style).forEach(key => {
104
+ const value = style[key];
105
+ if (value === undefined || value === null || value === '') return;
106
+ const useUnit = DIMENSION_PROPS.has(key)
107
+ ? addUnit(value as any, unit)
108
+ : value;
109
+ if (useUnit === undefined || useUnit === null || useUnit === '') return;
110
+ pairs.push(`${toKebab(key)}:${useUnit}`);
111
+ });
112
+ return pairs.join(';');
113
+ };
253
114
 
254
115
  /**
255
116
  * 容错 JSON 解析:字符串用 JSON.parse,其他保持原样或返回 null。
@@ -272,18 +133,11 @@ const getAbsLayout = (el: CardElement): AbsoluteLayoutDefinition | null =>
272
133
  ? (el.layout as AbsoluteLayoutDefinition)
273
134
  : null;
274
135
 
275
- const getFlexLayout = (el: CardElement): FlexItemLayoutDefinition | null =>
276
- el.layout && el.layout.mode === 'flex'
277
- ? (el.layout as FlexItemLayoutDefinition)
278
- : null;
279
-
280
136
  /** ---------- 布局与数据解析 ---------- */
281
137
 
282
138
  /**
283
139
  * 归一化布局输入(对象或 JSON 字符串),补齐宽高/容器/children 默认值。
284
140
  */
285
- export type CardLayoutInput = CardLayoutSchema[];
286
-
287
141
  export const normalizeLayout = (
288
142
  layout: CardLayoutInput
289
143
  ): CardLayoutSchema[] => {
@@ -318,41 +172,27 @@ const pathToSegments = (path: string): string[] =>
318
172
  /**
319
173
  * 按路径访问对象/数组,缺失时返回 undefined/null。
320
174
  */
321
- const readByPath = (data: any, path: string): any => {
322
- if (path === undefined || path === null || path === '') return data;
323
- const segments = pathToSegments(path);
324
- let cursor: any = data;
325
- for (let i = 0; i < segments.length; i += 1) {
326
- if (!isObject(cursor) && !Array.isArray(cursor)) return undefined;
327
- const key = segments[i];
328
- if (Array.isArray(cursor)) {
329
- const idx = Number(key);
330
- cursor = Number.isNaN(idx) ? undefined : cursor[idx];
331
- } else {
332
- cursor = (cursor as Record<string, any>)[key];
333
- }
334
- if (cursor === undefined || cursor === null) {
335
- return cursor;
336
- }
337
- }
338
- return cursor;
175
+ const readByPath = (data: any, path: string): any => {
176
+ if (path === undefined || path === null || path === '') return data;
177
+ const segments = pathToSegments(path);
178
+ let cursor: any = data;
179
+ for (let i = 0; i < segments.length; i += 1) {
180
+ if (!isObject(cursor) && !Array.isArray(cursor)) return undefined;
181
+ const key = segments[i];
182
+ if (Array.isArray(cursor)) {
183
+ const idx = Number(key);
184
+ cursor = Number.isNaN(idx) ? undefined : cursor[idx];
185
+ } else {
186
+ cursor = (cursor as Record<string, any>)[key];
187
+ }
188
+ if (cursor === undefined || cursor === null) {
189
+ return cursor;
190
+ }
191
+ }
192
+ return cursor;
339
193
  };
340
-
341
- export interface BindingContext {
342
- /**
343
- * 当前上下文数据的绑定前缀(repeatable-group 内为 dataPath)。
344
- *
345
- */
346
- contextBinding?: string;
347
- /**
348
- * 当前上下文数据(repeatable-group 单条数据)。
349
- */
350
- contextData?: any;
351
- }
352
194
  /**
353
- * 解析元素 binding 对应的数据:
354
- * - 全局 binding:直接基于根数据;
355
- * - repeatable-group:binding 等于/前缀 dataPath 或以 `$item.` 开头时,改用当前条目数据。
195
+ * Resolve element binding against provided data.
356
196
  */
357
197
  export const resolveBindingValue = (
358
198
  binding: string | undefined,
@@ -360,122 +200,49 @@ export const resolveBindingValue = (
360
200
  context?: BindingContext
361
201
  ): any => {
362
202
  if (!binding) return undefined;
363
- const { contextBinding, contextData } = context || {};
364
- let target: any = rootData;
365
- let path = binding;
366
-
367
- // repeatable-group: binding 等于 dataPath 或以其为前缀时,切换到当前条目数据
368
- if (
369
- contextBinding &&
370
- (binding === contextBinding || binding.startsWith(`${contextBinding}.`))
371
- ) {
372
- target = contextData;
373
- path =
374
- binding === contextBinding
375
- ? ''
376
- : binding.slice(contextBinding.length + 1);
377
- } else if (binding.startsWith('$item.')) {
378
- target = contextData;
379
- path = binding.slice('$item.'.length);
380
- }
381
-
382
- const value = readByPath(target, path);
203
+ const value = readByPath(rootData, binding);
383
204
  return value === undefined ? undefined : value;
384
205
  };
385
206
 
386
207
  /** ---------- 样式构建 ---------- */
387
208
 
388
- const justifyByTextAlign = (
389
- textAlign?: string
390
- ): 'flex-start' | 'center' | 'flex-end' => {
391
- if (textAlign === 'center') return 'center';
392
- if (textAlign === 'right') return 'flex-end';
393
- return 'flex-start';
394
- };
395
- /**
396
- * 生成元素外层样式(绝对/弹性布局),始终返回内联样式字符串。
397
- */
398
- const buildWrapperStyle = (el: CardElement, unit: 'px' | 'rpx'): string => {
399
- const abs = getAbsLayout(el);
400
- if (abs) {
401
- const textAlign = el?.style?.textAlign as string | undefined;
402
- return styleObjectToString(
403
- {
404
- position: 'absolute',
405
- left: addUnit(abs.x, unit),
406
- top: addUnit(abs.y, unit),
407
- width: addUnit(abs.width, unit),
408
- height: addUnit(abs.height, unit),
409
- zIndex: abs.zIndex,
410
- boxSizing: 'border-box',
411
- textAlign,
412
- },
413
- unit
414
- );
415
- }
416
-
417
- const flex = getFlexLayout(el);
418
- if (!flex) return '';
419
- const item:any = flex.item || {};
420
- return styleObjectToString(
421
- {
422
- width: addUnit(flex.width, unit),
423
- height: addUnit(flex.height, unit),
424
- order: item.order,
425
- flexGrow: item.flexGrow,
426
- flexShrink: item.flexShrink,
427
- flexBasis: item.flexBasis,
428
- alignSelf: item.alignSelf,
429
- boxSizing: 'border-box',
430
- textAlign: el?.style?.textAlign as string | undefined,
431
- },
432
- unit
433
- );
434
- };
435
209
  /**
436
- * padding 数组/数字转 CSS 缩写字符串。
210
+ * 生成元素外层样式(绝对/弹性布局),始终返回内联样式字符串。
437
211
  */
438
- const formatPadding = (
439
- padding: number | number[] | undefined,
440
- unit: 'px' | 'rpx'
441
- ) => {
442
- if (padding === undefined || padding === null) return undefined;
443
- if (typeof padding === 'number') return addUnit(padding, unit);
444
- if (Array.isArray(padding)) {
445
- const list = padding
446
- .filter(v => v !== undefined && v !== null)
447
- .map(v => addUnit(v as number, unit));
448
- if (!list.length) return undefined;
449
- if (list.length === 2) return `${list[0]} ${list[1]}`;
450
- if (list.length === 3) return `${list[0]} ${list[1]} ${list[2]}`;
451
- if (list.length >= 4) return `${list[0]} ${list[1]} ${list[2]} ${list[3]}`;
212
+ const buildWrapperStyle = (el: CardElement, unit: 'px' | 'rpx'): string => {
213
+ const abs = getAbsLayout(el);
214
+ if (abs) {
215
+ const textAlign = el?.style?.textAlign as string | undefined;
216
+ return styleObjectToString(
217
+ {
218
+ position: 'absolute',
219
+ left: addUnit(abs.x, unit),
220
+ top: addUnit(abs.y, unit),
221
+ width: addUnit(abs.width, unit),
222
+ height: addUnit(abs.height, unit),
223
+ zIndex: abs.zIndex,
224
+ boxSizing: 'border-box',
225
+ textAlign,
226
+ },
227
+ unit
228
+ );
452
229
  }
453
- return undefined;
230
+
231
+ return '';
454
232
  };
455
233
  /**
456
- * 构建 layout-panel 的容器样式(flex)。
234
+ * padding 数组/数字转 CSS 缩写字符串。
235
+ */
236
+ /**
237
+ * 构建 layout-panel 的容器样式(绝对布局容器)。
457
238
  */
458
239
  const buildPanelStyle = (
459
240
  el: LayoutPanelElement,
460
241
  unit: 'px' | 'rpx'
461
242
  ): string => {
462
- const options = (el.container && el.container.options) || {};
463
243
  const style = {
464
- display: 'flex',
465
- flexDirection: options.direction || 'row',
466
- flexWrap: options.wrap || 'nowrap',
467
- justifyContent: options.justifyContent,
468
- alignItems: options.alignItems,
469
- padding: formatPadding(options.padding as any, unit),
244
+ display: 'block',
470
245
  } as Record<string, any>;
471
-
472
- if (options.gap && typeof options.gap === 'number') {
473
- style.gap = addUnit(options.gap, unit);
474
- } else if (options.gap && isObject(options.gap)) {
475
- style.rowGap = addUnit((options.gap as any).row, unit);
476
- style.columnGap = addUnit((options.gap as any).column, unit);
477
- }
478
-
479
246
  return styleObjectToString(style, unit);
480
247
  };
481
248
  /**
@@ -489,91 +256,46 @@ const normalizeElementStyle = (
489
256
  return styleObjectToString(style, unit);
490
257
  };
491
258
 
492
- /** ---------- Repeatable 相关 ---------- */
493
- /**
494
- * 通过前两条 item 的布局推断间距,不足则回落 DEFAULT_GROUP_GAP。
495
- */
496
- const inferGroupGap = (group: RepeatableGroupElement): number => {
497
- const items = group.items || [];
498
- if (items.length >= 2) {
499
- const first = (items[0].elements || []).map(getAbsLayout).find(Boolean);
500
- const second = (items[1].elements || []).map(getAbsLayout).find(Boolean);
501
- if (
502
- first &&
503
- second &&
504
- typeof first.y === 'number' &&
505
- typeof second.y === 'number'
506
- ) {
507
- return Math.abs(second.y - first.y);
508
- }
509
- }
510
- return DEFAULT_GROUP_GAP;
511
- };
512
- /**
513
- * 展开 repeatable-group 为具体元素实例,并附带条目数据。
514
- * 优先使用设计器保存的 `items`;否则按推断间距克隆 `itemTemplate`。
515
- */
516
- const materializeRepeatableItems = (
517
- group: RepeatableGroupElement,
518
- rootData: Record<string, any>
519
- ): { element: CardElement; contextData: any; contextBinding: string }[] => {
520
- const result: {
521
- element: CardElement;
522
- contextData: any;
523
- contextBinding: string;
524
- }[] = [];
525
- const dataset = resolveBindingValue(group.dataPath, rootData, {}) || [];
526
- const dataList = Array.isArray(dataset) ? dataset : [];
527
- const maxItems =
528
- group.maxItems ||
529
- dataList.length ||
530
- (group.items || []).length ||
531
- (group.itemTemplate || []).length;
532
- const template =
533
- (group.items && group.items[0] && group.items[0].elements) ||
534
- group.itemTemplate ||
535
- [];
536
- const gap = inferGroupGap(group);
537
-
538
- // Use saved items (from designer) first
539
- if (group.items && group.items.length) {
540
- group.items.slice(0, maxItems).forEach((item, idx) => {
541
- const payload = dataList[idx] !== undefined ? dataList[idx] : item.data;
542
- (item.elements || []).forEach(el => {
543
- result.push({
544
- element: el,
545
- contextData: payload,
546
- contextBinding: group.dataPath,
547
- });
548
- });
549
- });
550
- return result;
551
- }
552
-
553
- // Otherwise clone from template by gap
554
- dataList.slice(0, maxItems).forEach((payload, idx) => {
555
- template.forEach(el => {
556
- const abs = getAbsLayout(el);
557
- const cloned = abs
558
- ? ({
559
- ...el,
560
- layout: { ...abs, y: abs.y + idx * gap },
561
- } as CardElement)
562
- : el;
563
- result.push({
564
- element: cloned,
565
- contextData: payload,
566
- contextBinding: group.dataPath,
567
- });
568
- });
569
- });
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);
570
285
 
571
- 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
+ };
572
294
  };
573
295
 
574
296
  /** ---------- 渲染树构建 ---------- */
575
297
  /**
576
- * 将 children 展开为渲染节点,处理 repeatable-group 展开与 mutualExcludes 隐藏。
298
+ * 将 children 展开为渲染节点。
577
299
  */
578
300
  export const buildRenderNodes = (
579
301
  children: CardElement[],
@@ -583,34 +305,9 @@ export const buildRenderNodes = (
583
305
  ): RenderNode[] => {
584
306
  if (!Array.isArray(children)) return [];
585
307
 
586
- // Mark mutually exclusive bindings to hide them when repeatable-group is present
587
- const excluded = new Set<string>();
588
- children.forEach(el => {
589
- if (el && el.type === 'repeatable-group' && !!el.visible) {
590
- (el.mutualExcludes || []).forEach(b => excluded.add(b));
591
- }
592
- });
593
-
594
308
  const nodes: RenderNode[] = [];
595
309
  children.forEach(el => {
596
310
  if (!el || el.visible === false) return;
597
- if (el.type === 'repeatable-group') {
598
- const instances = materializeRepeatableItems(
599
- el as RepeatableGroupElement,
600
- rootData
601
- );
602
- instances.forEach(({ element, contextData, contextBinding }) => {
603
- const node = buildNode(element, rootData, unit, {
604
- ...context,
605
- contextData,
606
- contextBinding,
607
- });
608
- if (node) nodes.push(node);
609
- });
610
- return;
611
- }
612
-
613
- if (el.binding && excluded.has(el.binding)) return;
614
311
  const node = buildNode(el, rootData, unit, context);
615
312
  if (node) nodes.push(node);
616
313
  });
@@ -655,6 +352,7 @@ const buildNode = (
655
352
  contentStyle: textStyle,
656
353
  text: `${value}`,
657
354
  visible: !!el.visible,
355
+ icon: buildTextIcon(el as TextElement, unit),
658
356
  };
659
357
  }
660
358
 
@@ -677,22 +375,22 @@ const buildNode = (
677
375
  };
678
376
  }
679
377
 
680
- if (el.type === 'icon') {
681
- const name =
682
- resolveBindingValue(el.binding, rootData, context) ??
683
- (el as IconElement).name ??
684
- el.defaultValue ??
685
- '';
686
- return {
687
- id: el.id,
688
- type: el.type,
689
- wrapperStyle,
690
- contentStyle: baseStyle,
691
- name: `${name}`,
692
- text: `${name}`,
693
- visible: !!el.visible,
694
- };
695
- }
378
+ if (el.type === 'icon') {
379
+ const name =
380
+ resolveBindingValue(el.binding, rootData, context) ??
381
+ (el as IconElement).name ??
382
+ el.defaultValue ??
383
+ '';
384
+ return {
385
+ id: el.id,
386
+ type: el.type,
387
+ wrapperStyle,
388
+ contentStyle: baseStyle,
389
+ name: `${name}`,
390
+ text: `${name}`,
391
+ visible: !!el.visible,
392
+ };
393
+ }
696
394
 
697
395
  if (el.type === 'custom') {
698
396
  return {
@@ -704,15 +402,7 @@ const buildNode = (
704
402
  };
705
403
  }
706
404
 
707
- // Unknown type fallback to simple text node
708
- return {
709
- id: el.id,
710
- type: 'text',
711
- wrapperStyle,
712
- contentStyle: baseStyle,
713
- text: el.defaultValue || '',
714
- visible: !!el.visible,
715
- };
405
+ return null;
716
406
  };
717
407
  /**
718
408
  * 主入口:合并布局 Schema 与数据,生成供前端使用的渲染结果。