km-card-layout-component-miniprogram 0.1.5 → 0.1.7
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/example/pages/home/index.js +4 -250
- package/miniprogram_dist/components/card-layout/index.js +214 -29
- package/miniprogram_dist/components/card-layout/index.wxml +29 -5
- package/miniprogram_dist/components/card-layout/index.wxss +28 -6
- package/miniprogram_dist/vendor/km-card-layout-core/index.js +58 -181
- package/miniprogram_dist/vendor/km-card-layout-core/interface/data/payload.js +2 -0
- package/miniprogram_dist/vendor/km-card-layout-core/interface/elements.js +2 -0
- package/miniprogram_dist/vendor/km-card-layout-core/interface/index.js +20 -0
- package/miniprogram_dist/vendor/km-card-layout-core/interface/layout.js +2 -0
- package/miniprogram_dist/vendor/km-card-layout-core/interface/render.js +2 -0
- package/miniprogram_dist/vendor/km-card-layout-core/utils.js +132 -0
- package/package.json +1 -1
- package/script/sync-core.js +10 -2
- package/src/components/card-layout/index.ts +340 -56
- package/src/components/card-layout/index.wxml +29 -5
- package/src/components/card-layout/index.wxss +28 -6
- package/src/vendor/km-card-layout-core/index.ts +153 -461
- package/src/vendor/km-card-layout-core/interface/data/payload.ts +45 -0
- package/src/vendor/km-card-layout-core/interface/elements.ts +73 -0
- package/src/vendor/km-card-layout-core/interface/index.ts +4 -0
- package/src/vendor/km-card-layout-core/interface/layout.ts +19 -0
- package/src/vendor/km-card-layout-core/interface/render.ts +52 -0
- package/src/vendor/km-card-layout-core/types.d.ts +22 -154
- package/src/vendor/km-card-layout-core/utils.ts +141 -0
|
@@ -5,167 +5,29 @@
|
|
|
5
5
|
* - 职责:将布局 Schema 与业务数据合成为带内联样式的渲染树,外层只需将节点映射到各端组件。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
210
|
+
* 生成元素外层样式(绝对/弹性布局),始终返回内联样式字符串。
|
|
437
211
|
*/
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
230
|
+
|
|
231
|
+
return '';
|
|
454
232
|
};
|
|
455
233
|
/**
|
|
456
|
-
*
|
|
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: '
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 与数据,生成供前端使用的渲染结果。
|
|
@@ -766,3 +456,5 @@ export const buildRenderResult = (
|
|
|
766
456
|
};
|
|
767
457
|
});
|
|
768
458
|
};
|
|
459
|
+
|
|
460
|
+
export * from './utils';
|